From 5a8a6c1ba3510ffce40b0dbc01e1ceee20265d72 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Tue, 24 Jun 2025 11:41:30 -0700 Subject: [PATCH] Initial commit --- .github/ISSUE_TEMPLATE/bug_report.md | 32 + .github/ISSUE_TEMPLATE/feature_request.md | 22 + .github/PULL_REQUEST_TEMPLATE.md | 27 + .github/workflows/deploy-demo.yml | 68 + .gitignore | 42 + .npmignore | 40 + CODE_OF_CONDUCT.md | 128 + CONTRIBUTING.md | 86 + LICENSE | 21 + README.demo.md | 59 + README.md | 1285 +++ brainy.png | Bin 0 -> 222935 bytes cli-wrapper.js | 56 + cloud-wrapper/.env.example | 24 + cloud-wrapper/README.md | 341 + cloud-wrapper/package-lock.json | 4082 ++++++++ cloud-wrapper/package.json | 92 + cloud-wrapper/scripts/deploy-aws.js | 205 + cloud-wrapper/scripts/deploy-cloudflare.js | 245 + cloud-wrapper/scripts/deploy-gcp.js | 196 + cloud-wrapper/src/index.ts | 106 + cloud-wrapper/src/routes.ts | 186 + cloud-wrapper/src/services/brainyService.ts | 97 + cloud-wrapper/src/services/mcpService.ts | 204 + cloud-wrapper/src/websocket.ts | 433 + cloud-wrapper/tsconfig.json | 17 + demo/index.html | 4255 +++++++++ encoded-image.html | 1 + encoded-image.txt | 1 + index.html | 17 + package-lock.json | 8315 +++++++++++++++++ package.json | 154 + rollup.config.js | 179 + scalingStrategy.md | 77 + scripts/encode-image.js | 47 + scripts/generate-version.js | 107 + scripts/update-readme.js | 26 + src/augmentationFactory.ts | 628 ++ src/augmentationPipeline.ts | 709 ++ src/augmentationRegistry.ts | 122 + src/augmentationRegistryLoader.ts | 291 + src/augmentations/README.md | 251 + src/augmentations/conduitAugmentations.ts | 1409 +++ src/augmentations/memoryAugmentations.ts | 333 + .../serverSearchAugmentations.ts | 665 ++ src/brainyData.ts | 2252 +++++ src/cli.ts | 1286 +++ src/coreTypes.ts | 163 + src/examples/basicUsage.ts | 163 + src/global.d.ts | 15 + src/hnsw/hnswIndex.ts | 704 ++ src/hnsw/hnswIndexOptimized.ts | 586 ++ src/index.ts | 351 + src/mcp/README.md | 180 + src/mcp/brainyMCPAdapter.ts | 203 + src/mcp/brainyMCPService.ts | 347 + src/mcp/index.ts | 19 + src/mcp/mcpAugmentationToolset.ts | 225 + src/pipeline.ts | 919 ++ src/sequentialPipeline.ts | 572 ++ src/storage/fileSystemStorage.ts | 931 ++ src/storage/opfsStorage.ts | 1890 ++++ src/storage/s3CompatibleStorage.ts | 1106 +++ src/types/augmentations.ts | 413 + src/types/brainyDataInterface.ts | 55 + src/types/fileSystemTypes.ts | 14 + src/types/graphTypes.ts | 150 + src/types/mcpTypes.ts | 149 + src/types/pipelineTypes.ts | 33 + src/types/tensorflowTypes.ts | 14 + src/unified.ts | 36 + src/utils/distance.ts | 86 + src/utils/embedding.ts | 293 + src/utils/environment.ts | 57 + src/utils/index.ts | 2 + src/utils/version.ts | 6 + src/utils/workerUtils.ts | 145 + tsconfig.browser.json | 30 + tsconfig.json | 29 + tsconfig.unified.json | 29 + verify-package-size.js | 135 + 81 files changed, 39269 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/deploy-demo.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.demo.md create mode 100644 README.md create mode 100644 brainy.png create mode 100755 cli-wrapper.js create mode 100644 cloud-wrapper/.env.example create mode 100644 cloud-wrapper/README.md create mode 100644 cloud-wrapper/package-lock.json create mode 100644 cloud-wrapper/package.json create mode 100644 cloud-wrapper/scripts/deploy-aws.js create mode 100644 cloud-wrapper/scripts/deploy-cloudflare.js create mode 100644 cloud-wrapper/scripts/deploy-gcp.js create mode 100644 cloud-wrapper/src/index.ts create mode 100644 cloud-wrapper/src/routes.ts create mode 100644 cloud-wrapper/src/services/brainyService.ts create mode 100644 cloud-wrapper/src/services/mcpService.ts create mode 100644 cloud-wrapper/src/websocket.ts create mode 100644 cloud-wrapper/tsconfig.json create mode 100644 demo/index.html create mode 100644 encoded-image.html create mode 100644 encoded-image.txt create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 rollup.config.js create mode 100644 scalingStrategy.md create mode 100644 scripts/encode-image.js create mode 100755 scripts/generate-version.js create mode 100644 scripts/update-readme.js create mode 100644 src/augmentationFactory.ts create mode 100644 src/augmentationPipeline.ts create mode 100644 src/augmentationRegistry.ts create mode 100644 src/augmentationRegistryLoader.ts create mode 100644 src/augmentations/README.md create mode 100644 src/augmentations/conduitAugmentations.ts create mode 100644 src/augmentations/memoryAugmentations.ts create mode 100644 src/augmentations/serverSearchAugmentations.ts create mode 100644 src/brainyData.ts create mode 100644 src/cli.ts create mode 100644 src/coreTypes.ts create mode 100644 src/examples/basicUsage.ts create mode 100644 src/global.d.ts create mode 100644 src/hnsw/hnswIndex.ts create mode 100644 src/hnsw/hnswIndexOptimized.ts create mode 100644 src/index.ts create mode 100644 src/mcp/README.md create mode 100644 src/mcp/brainyMCPAdapter.ts create mode 100644 src/mcp/brainyMCPService.ts create mode 100644 src/mcp/index.ts create mode 100644 src/mcp/mcpAugmentationToolset.ts create mode 100644 src/pipeline.ts create mode 100644 src/sequentialPipeline.ts create mode 100644 src/storage/fileSystemStorage.ts create mode 100644 src/storage/opfsStorage.ts create mode 100644 src/storage/s3CompatibleStorage.ts create mode 100644 src/types/augmentations.ts create mode 100644 src/types/brainyDataInterface.ts create mode 100644 src/types/fileSystemTypes.ts create mode 100644 src/types/graphTypes.ts create mode 100644 src/types/mcpTypes.ts create mode 100644 src/types/pipelineTypes.ts create mode 100644 src/types/tensorflowTypes.ts create mode 100644 src/unified.ts create mode 100644 src/utils/distance.ts create mode 100644 src/utils/embedding.ts create mode 100644 src/utils/environment.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/version.ts create mode 100644 src/utils/workerUtils.ts create mode 100644 tsconfig.browser.json create mode 100644 tsconfig.json create mode 100644 tsconfig.unified.json create mode 100755 verify-package-size.js diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..91035681 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Bug Description +A clear and concise description of what the bug is. + +## Reproduction Steps +Steps to reproduce the behavior: +1. Initialize BrainyData with '...' +2. Call method '....' +3. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Environment +- Brainy version: [e.g. 0.9.4] +- Environment: [e.g. Browser, Node.js, serverless] +- Browser (if applicable): [e.g. Chrome, Safari] +- Node.js version (if applicable): [e.g. 23.11.0] +- Operating System: [e.g. Windows 10, macOS Monterey, Ubuntu 22.04] + +## Additional Context +Add any other context about the problem here. If applicable, include code snippets, error messages, or screenshots. + +## Possible Solution +If you have suggestions on how to fix the issue, please describe them here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..647a54e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Problem Statement +A clear and concise description of what problem this feature would solve. For example: "I'm always frustrated when [...]" + +## Proposed Solution +A clear and concise description of what you want to happen. + +## Alternative Solutions +A clear and concise description of any alternative solutions or features you've considered. + +## Use Case +Describe a concrete use case that highlights the value of this feature. + +## Additional Context +Add any other context, code examples, or references about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..7bc8b5bb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +## Description +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. + +Fixes # (issue) + +## Type of change +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Performance improvement +- [ ] Code refactoring (no functional changes) + +## How Has This Been Tested? +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. + +## Checklist: +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml new file mode 100644 index 00000000..01ff0a7b --- /dev/null +++ b/.github/workflows/deploy-demo.yml @@ -0,0 +1,68 @@ +name: Deploy Demo to GitHub Pages + +on: + push: + branches: [ main ] + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v4 + + - name: Setup Node.js 🔧 + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies 📦 + run: npm install --legacy-peer-deps + + - name: Build project 🏗️ + run: | + npm run build + npm run build:browser + + - name: Prepare deployment 📦 + run: | + mkdir -p _site + mkdir -p _site/demo + mkdir -p _site/dist + cp index.html _site/ + cp demo/index.html _site/demo/ + cp -r dist/* _site/dist/ + cp brainy.png _site/ + # Copy dist directly to demo/dist for easier access + mkdir -p _site/demo/dist + cp -r dist/* _site/demo/dist/ + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d4b808ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Build output +/tmp +/out-tsc +/dist +/cloud-wrapper/dist + +# Dependencies +/node_modules +/cloud-wrapper/node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +/.pnp +.pnp.js + +# Coverage directory +/coverage + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.iml +*.iws +*.ipr +*.sublime-workspace +*.sublime-project + +# OS files +.DS_Store +Thumbs.db + +# Data directories created by FileSystemStorage +/brainy-data +/custom-data +/clean-history.sh diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..fc9e8b71 --- /dev/null +++ b/.npmignore @@ -0,0 +1,40 @@ +# Exclude source maps +*.map +**/*.map + +# Development files +node_modules/ +src/ +tests/ +examples/ +.github/ +.vscode/ +.idea/ +cloud-wrapper/ +scripts/ + +# Configuration files +.eslintrc +.prettierrc +tsconfig*.json +rollup.config.js +jest.config.js + +# Build artifacts +emocoverage/ +.nyc_output/ + +# Large files +# Include the logo but exclude other PNGs +!brainy.png +*.png +encoded-image.* +README.demo.md +scalingStrategy.md + +# Misc +.DS_Store +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..77c73d96 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Project maintainers are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned with this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project maintainers responsible for enforcement at +conduct@soulcraft.com. +All complaints will be reviewed and investigated promptly and fairly. + +All project maintainers are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Project maintainers will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from project maintainers, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..edbe7285 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +
+Brainy Logo + +# Contributing to Brainy + +
+ +Thank you for your interest in contributing to Brainy! This document provides guidelines and instructions for +contributing to the project. + +We welcome contributions of all kinds, including bug fixes, feature additions, documentation improvements, and more. +By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). + +## Commit Message Guidelines + +When contributing to this project, please write clear and descriptive commit messages that explain the purpose of your +changes. Good commit messages help maintainers understand your contributions and make the review process smoother. + +### Best Practices + +- Keep the first line concise (ideally under 50 characters) +- Use the imperative mood ("Add feature" not "Added feature") +- Reference issues and pull requests where appropriate +- When necessary, provide more detailed explanations in the commit body + +### Examples + +``` +Add vector normalization option +Fix distance calculation in HNSW search +Update API documentation +Add support for IndexedDB storage +Change API parameter order +Simplify vector comparison logic +Update build dependencies +``` + +## Pull Request Process + +1. Ensure your code follows the project's coding standards +2. Update the documentation if necessary +3. Use conventional commit messages in your PR +4. Your PR will be reviewed by maintainers and merged if approved + +## Development Setup + +1. Fork and clone the repository +2. Install dependencies: `npm install` +3. Build the project: `npm run build` + +## Code Style + +This project uses ESLint and Prettier for code formatting and style checking. The configuration can be found in the `package.json` file. Please ensure your code follows these standards: + +- Use 2 spaces for indentation +- Use single quotes for strings +- No semicolons +- Trailing commas are not used +- Maximum line length is 80 characters + +You can check your code style by running: +```bash +npx eslint src/ +``` + +## Branching Strategy + +- `main` - The main branch contains the latest stable release +- `develop` - The development branch contains the latest development changes +- Feature branches - Create a branch from `develop` for your feature or fix + +When working on a new feature or fix: +1. Create a new branch from `develop` with a descriptive name (e.g., `feature/add-vector-normalization` or `fix/distance-calculation`) +2. Make your changes in that branch +3. Submit a pull request to merge your branch into `develop` + +## Issue Reporting + +Before submitting a new issue, please search existing issues to avoid duplicates. + +- For bugs, use the bug report template +- For feature requests, use the feature request template +- Be as detailed as possible in your description +- Include code examples, error messages, and screenshots if applicable + +Thank you for contributing to Brainy! diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..681d9d76 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Soulcraft Research + +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. diff --git a/README.demo.md b/README.demo.md new file mode 100644 index 00000000..9a706bb8 --- /dev/null +++ b/README.demo.md @@ -0,0 +1,59 @@ +# Running the Brainy Demo + +The Brainy interactive demo showcases the library's features in a web browser. Follow these steps to run it: + +## Prerequisites + +- Make sure you have Node.js installed (version 23.11.0 or higher) +- Ensure the project is built (`npm run build:all`) + +## Running the Demo + +### Option 1: Using the npm script (recommended) + +Run the following command from the project root: + +```bash +npm run demo +``` + +This will start an HTTP server and automatically open the demo in your default browser. + +### Option 2: Manual setup + +1. Start an HTTP server in the project root: + +```bash +npx http-server +``` + +2. Open your browser and navigate to: + http://localhost:8080/demo/index.html + +## Troubleshooting + +If you see the error "Could not load Brainy library. Please ensure the project is built and served over HTTP", check the +following: + +1. Make sure you've built the project with `npm run build:all` +2. Ensure you're accessing the demo through HTTP (not by opening the file directly) +3. Check your browser's console for additional error messages + +If issues persist, try clearing your browser cache or using a private/incognito window. + +## Build Process + +The Brainy library uses a two-step build process: + +1. `npm run build` - Compiles TypeScript files to JavaScript (used for Node.js environments) +2. `npm run build:browser` - Creates a browser-compatible bundle using Rollup + +You can run both steps together with: + +```bash +npm run build:all +``` + +The browser bundle is created from `src/unified.ts`, which provides environment detection and adapts to browser, +Node.js, or serverless environments. This unified approach ensures that the library works correctly across all +environments. diff --git a/README.md b/README.md new file mode 100644 index 00000000..82f9c593 --- /dev/null +++ b/README.md @@ -0,0 +1,1285 @@ +
+Brainy Logo +

+ +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) +[![Node.js](https://img.shields.io/badge/node-%3E%3D23.11.0-brightgreen.svg)](https://nodejs.org/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.1.6-blue.svg)](https://www.typescriptlang.org/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) +[![npm](https://img.shields.io/badge/npm-v0.9.10-blue.svg)](https://www.npmjs.com/package/@soulcraft/brainy) + +[//]: # ([![Cartographer](https://img.shields.io/badge/Cartographer-Official%20Standard-brightgreen)](https://github.com/sodal-project/cartographer)) + +**A powerful graph & vector data platform for AI applications across any environment** + +
+ +## ✨ Overview + +Brainy combines the power of vector search with graph relationships in a lightweight, cross-platform database. Whether +you're building AI applications, recommendation systems, or knowledge graphs, Brainy provides the tools you need to +store, connect, and retrieve your data intelligently. + +What makes Brainy special? It intelligently adapts to your environment! Brainy automatically detects your platform, +adjusts its storage strategy, and optimizes performance based on your usage patterns. The more you use it, the smarter +it gets - learning from your data to provide increasingly relevant results and connections. + +### 🚀 Key Features + +- **Run Everywhere** - Works in browsers, Node.js, serverless functions, and containers +- **Vector Search** - Find semantically similar content using embeddings +- **Graph Relationships** - Connect data with meaningful relationships +- **Streaming Pipeline** - Process data in real-time as it flows through the system +- **Extensible Augmentations** - Customize and extend functionality with pluggable components +- **Built-in Conduits** - Sync and scale across instances with WebSocket and WebRTC +- **TensorFlow Integration** - Use TensorFlow.js for high-quality embeddings +- **Adaptive Intelligence** - Automatically optimizes for your environment and usage patterns +- **Persistent Storage** - Data persists across sessions and scales to any size +- **TypeScript Support** - Fully typed API with generics +- **CLI Tools** - Powerful command-line interface for data management +- **Model Control Protocol (MCP)** - Allow external AI models to access Brainy data and use augmentation pipeline as + tools + +## 🚀 Live Demo + +**[Try the live demo](https://soulcraft-research.github.io/brainy/demo/index.html)** - Check out the interactive demo on +GitHub Pages that showcases Brainy's main features. + +## 📊 What Can You Build? + +- **Semantic Search Engines** - Find content based on meaning, not just keywords +- **Recommendation Systems** - Suggest similar items based on vector similarity +- **Knowledge Graphs** - Build connected data structures with relationships +- **AI Applications** - Store and retrieve embeddings for machine learning models +- **AI-Enhanced Applications** - Build applications that leverage vector embeddings for intelligent data processing +- **Data Organization Tools** - Automatically categorize and connect related information +- **Adaptive Experiences** - Create applications that learn and evolve with your users +- **Model-Integrated Systems** - Connect external AI models to Brainy data and tools using MCP + +## 🔧 Installation + +```bash +npm install @soulcraft/brainy +``` + +TensorFlow.js packages are included as required dependencies and will be automatically installed. If you encounter +dependency conflicts, you may need to use the `--legacy-peer-deps` flag: + +```bash +npm install @soulcraft/brainy --legacy-peer-deps +``` + +## 🏁 Quick Start + +Brainy uses a unified build that automatically adapts to your environment (Node.js, browser, or serverless): + +```typescript +import { BrainyData, NounType, VerbType } from '@soulcraft/brainy' + +// Create and initialize the database +const db = new BrainyData() +await db.init() + +// Add data (automatically converted to vectors) +const catId = await db.add("Cats are independent pets", { + noun: NounType.Thing, + category: 'animal' +}) + +const dogId = await db.add("Dogs are loyal companions", { + noun: NounType.Thing, + category: 'animal' +}) + +// Search for similar items +const results = await db.searchText("feline pets", 2) +console.log(results) + +// Add a relationship between items +await db.addVerb(catId, dogId, { + verb: VerbType.RelatedTo, + description: 'Both are common household pets' +}) +``` + +### Import Options + +```typescript +// Standard import - automatically adapts to any environment +import { BrainyData } from '@soulcraft/brainy' + +// Minified version for production +import { BrainyData } from '@soulcraft/brainy/min' +``` + +### Browser Usage + +```html + + +``` + +Modern bundlers like Webpack, Rollup, and Vite will automatically use the unified build which adapts to any environment. + +## 🧩 How It Works + +Brainy combines four key technologies to create its adaptive intelligence: + +1. **Vector Embeddings** - Converts data (text, images, etc.) into numerical vectors that capture semantic meaning +2. **HNSW Algorithm** - Enables fast similarity search through a hierarchical graph structure +3. **Adaptive Environment Detection** - Automatically senses your platform and optimizes accordingly: + - Detects browser, Node.js, and serverless environments + - Adjusts performance parameters based on available resources + - Learns from query patterns to optimize future searches + - Tunes itself for your specific use cases +4. **Intelligent Storage Selection** - Uses the best available storage option for your environment: + - Browser: Origin Private File System (OPFS) + - Node.js: File system + - Server: S3-compatible storage (optional) + - Serverless: In-memory storage with optional cloud persistence + - Fallback: In-memory storage + - Automatically migrates between storage types as needed + +## 🚀 The Brainy Pipeline + +Brainy's data processing pipeline transforms raw data into searchable, connected knowledge that gets smarter over time: + +``` +Raw Data → Embedding → Vector Storage → Graph Connections → Adaptive Learning → Query & Retrieval +``` + +Each time data flows through this pipeline, Brainy learns more about your usage patterns and environment, making future +operations faster and more relevant. + +### Pipeline Stages + +1. **Data Ingestion** + - Raw text or pre-computed vectors enter the pipeline + - Data is validated and prepared for processing + +2. **Embedding Generation** + - Text is transformed into numerical vectors using embedding models + - Uses TensorFlow Universal Sentence Encoder for high-quality text embeddings + - Custom embedding functions can be plugged in for specialized domains + +3. **Vector Indexing** + - Vectors are indexed using the HNSW algorithm + - Hierarchical structure enables fast similarity search + - Configurable parameters for precision vs. performance tradeoffs + +4. **Graph Construction** + - Nouns (entities) become nodes in the knowledge graph + - Verbs (relationships) connect related entities + - Typed relationships add semantic meaning to connections + +5. **Adaptive Learning** + - Analyzes usage patterns to optimize future operations + - Tunes performance parameters based on your environment + - Adjusts search strategies based on query history + - Becomes more efficient and relevant the more you use it + +6. **Intelligent Storage** + - Data is saved using the optimal storage for your environment + - Automatic selection between OPFS, filesystem, S3, or memory + - Migrates between storage types as your application's needs evolve + - Scales from tiny datasets to massive data collections + - Configurable storage adapters for custom persistence needs + +### Augmentation Types + +Brainy uses a powerful augmentation system to extend functionality. Augmentations are processed in the following order: + +1. **SENSE** + - Ingests and processes raw, unstructured data into nouns and verbs + - Handles text, images, audio streams, and other input formats + - Example: Converting raw text into structured entities + +2. **MEMORY** + - Provides storage capabilities for data in different formats + - Manages persistence across sessions + - Example: Storing vectors in OPFS or filesystem + +3. **COGNITION** + - Enables advanced reasoning, inference, and logical operations + - Analyzes relationships between entities + - Examples: + - Inferring new connections between existing data + - Deriving insights from graph relationships + +4. **CONDUIT** + - Establishes channels for structured data exchange + - Connects with external systems and syncs between Brainy instances + - Two built-in iConduit augmentations for scaling out and syncing: + - **WebSocket iConduit** - Syncs data between browsers and servers + - **WebRTC iConduit** - Direct peer-to-peer syncing between browsers + - Examples: + - Integrating with third-party APIs + - Syncing Brainy instances between browsers using WebSockets + - Peer-to-peer syncing between browsers using WebRTC + +5. **ACTIVATION** + - Initiates actions, responses, or data manipulations + - Triggers events based on data changes + - Example: Sending notifications when new data is processed + +6. **PERCEPTION** + - Interprets, contextualizes, and visualizes identified nouns and verbs + - Creates meaningful representations of data + - Example: Generating visualizations of graph relationships + +7. **DIALOG** + - Facilitates natural language understanding and generation + - Enables conversational interactions + - Example: Processing user queries and generating responses + +8. **WEBSOCKET** + - Enables real-time communication via WebSockets + - Can be combined with other augmentation types + - Example: Streaming data processing in real-time + +### Streaming Data Support + +Brainy's pipeline is designed to handle streaming data efficiently: + +1. **WebSocket Integration** + - Built-in support for WebSocket connections + - Process data as it arrives without blocking + - Example: `setupWebSocketPipeline(url, dataType, options)` + +2. **Asynchronous Processing** + - Non-blocking architecture for real-time data handling + - Parallel processing of incoming streams + - Example: `createWebSocketHandler(connection, dataType, options)` + +3. **Event-Based Architecture** + - Augmentations can listen to data feeds and streams + - Real-time updates propagate through the pipeline + - Example: `listenToFeed(feedUrl, callback)` + +4. **Threaded Execution** + - Comprehensive multi-threading for high-performance operations + - Parallel processing for batch operations, vector calculations, and embedding generation + - Configurable execution modes (SEQUENTIAL, PARALLEL, THREADED) + - Automatic thread management based on environment capabilities + - Example: `executeTypedPipeline(augmentations, method, args, { mode: ExecutionMode.THREADED })` + +### Build System + +Brainy uses a modern build system that optimizes for both Node.js and browser environments: + +1. **ES Modules** + - Built as ES modules for maximum compatibility + - Works in modern browsers and Node.js environments + - Separate optimized builds for browser and Node.js + +2. **Environment-Specific Builds** + - **Node.js Build**: Optimized for server environments with full functionality + - **Browser Build**: Optimized for browser environments with reduced bundle size + - Conditional exports in package.json for automatic environment detection + +3. **Environment Detection** + - Automatically detects whether it's running in a browser or Node.js + - Loads appropriate dependencies and functionality based on the environment + - Provides consistent API across all environments + +4. **TypeScript** + - Written in TypeScript for type safety and better developer experience + - Generates type definitions for TypeScript users + - Compiled to ES2020 for modern JavaScript environments + +5. **Build Scripts** + - `npm run build`: Builds the Node.js version + - `npm run build:browser`: Builds the browser-optimized version + - `npm run build:all`: Builds both versions + - `npm run demo`: Builds all versions and starts a demo server + - GitHub Actions workflow: Automatically deploys the demo directory to GitHub Pages when pushing to the main branch + +### Running the Pipeline + +The pipeline runs automatically when you: + +```typescript +// Add data (runs embedding → indexing → storage) +const id = await db.add("Your text data here", { metadata }) + +// Search (runs embedding → similarity search) +const results = await db.searchText("Your query here", 5) + +// Connect entities (runs graph construction → storage) +await db.addVerb(sourceId, targetId, { verb: VerbType.RelatedTo }) +``` + +Using the CLI: + +```bash +# Add data through the CLI pipeline +brainy add "Your text data here" '{"noun":"Thing"}' + +# Search through the CLI pipeline +brainy search "Your query here" --limit 5 + +# Connect entities through the CLI +brainy addVerb RelatedTo +``` + +### Extending the Pipeline + +Brainy's pipeline is designed for extensibility at every stage: + +1. **Custom Embedding** + ```typescript + // Create your own embedding function + const myEmbedder = async (text) => { + // Your custom embedding logic here + return [0.1, 0.2, 0.3, ...] // Return a vector + } + + // Use it in Brainy + const db = new BrainyData({ + embeddingFunction: myEmbedder + }) + ``` + +2. **Custom Distance Functions** + ```typescript + // Define your own distance function + const myDistance = (a, b) => { + // Your custom distance calculation + return Math.sqrt(a.reduce((sum, val, i) => sum + Math.pow(val - b[i], 2), 0)) + } + + // Use it in Brainy + const db = new BrainyData({ + distanceFunction: myDistance + }) + ``` + +3. **Custom Storage Adapters** + ```typescript + // Implement the StorageAdapter interface + class MyStorage implements StorageAdapter { + // Your storage implementation + } + + // Use it in Brainy + const db = new BrainyData({ + storageAdapter: new MyStorage() + }) + ``` + +4. **Augmentations System** + ```typescript + // Create custom augmentations to extend functionality + const myAugmentation = { + type: 'memory', + name: 'my-custom-storage', + // Implementation details + } + + // Register with Brainy + db.registerAugmentation(myAugmentation) + ``` + +## Data Model + +Brainy uses a graph-based data model with two primary concepts: + +### Nouns (Entities) + +The main entities in your data (nodes in the graph): + +- Each noun has a unique ID, vector representation, and metadata +- Nouns can be categorized by type (Person, Place, Thing, Event, Concept, etc.) +- Nouns are automatically vectorized for similarity search + +### Verbs (Relationships) + +Connections between nouns (edges in the graph): + +- Each verb connects a source noun to a target noun +- Verbs have types that define the relationship (RelatedTo, Controls, Contains, etc.) +- Verbs can have their own metadata to describe the relationship + +## Command Line Interface + +Brainy includes a powerful CLI for managing your data: + +```bash +# Install globally +npm install -g @soulcraft/brainy --legacy-peer-deps + +# Initialize a database +brainy init + +# Add some data +brainy add "Cats are independent pets" '{"noun":"Thing","category":"animal"}' +brainy add "Dogs are loyal companions" '{"noun":"Thing","category":"animal"}' + +# Search for similar items +brainy search "feline pets" 5 + +# Add relationships between items +brainy addVerb RelatedTo '{"description":"Both are pets"}' + +# Visualize the graph structure +brainy visualize +brainy visualize --root --depth 3 +``` + +### Development Usage + +```bash +# Run the CLI directly from the source +npm run cli help + +# Generate a random graph for testing +npm run cli generate-random-graph --noun-count 20 --verb-count 40 +``` + +### Available Commands + +#### Basic Database Operations: + +- `init` - Initialize a new database +- `add [metadata]` - Add a new noun with text and optional metadata +- `search [limit]` - Search for nouns similar to the query +- `get ` - Get a noun by ID +- `delete ` - Delete a noun by ID +- `addVerb [metadata]` - Add a relationship +- `getVerbs ` - Get all relationships for a noun +- `status` - Show database status +- `clear` - Clear all data from the database +- `generate-random-graph` - Generate test data +- `visualize` - Visualize the graph structure +- `completion-setup` - Setup shell autocomplete + +#### Pipeline and Augmentation Commands: + +- `list-augmentations` - List all available augmentation types and registered augmentations +- `augmentation-info ` - Get detailed information about a specific augmentation type +- `test-pipeline [text]` - Test the sequential pipeline with sample data + - `-t, --data-type ` - Type of data to process (default: 'text') + - `-m, --mode ` - Execution mode: sequential, parallel, threaded (default: 'sequential') + - `-s, --stop-on-error` - Stop execution if an error occurs + - `-v, --verbose` - Show detailed output +- `stream-test` - Test streaming data through the pipeline (simulated) + - `-c, --count ` - Number of data items to stream (default: 5) + - `-i, --interval ` - Interval between data items in milliseconds (default: 1000) + - `-t, --data-type ` - Type of data to process (default: 'text') + - `-v, --verbose` - Show detailed output + +## API Reference + +### Database Management + +```typescript +// Initialize the database +await db.init() + +// Clear all data +await db.clear() + +// Get database status +const status = await db.status() + +// Backup all data from the database +const backupData = await db.backup() + +// Restore data into the database +const restoreResult = await db.restore(backupData, { clearExisting: true }) +``` + +### Working with Nouns (Entities) + +```typescript +// Add a noun (automatically vectorized) +const id = await db.add(textOrVector, { + noun: NounType.Thing, + // other metadata... +}) + +// Add multiple nouns in parallel (with multithreading) +const ids = await db.addBatch([ + { + vectorOrData: "First item to add", + metadata: { noun: NounType.Thing, category: 'example' } + }, + { + vectorOrData: "Second item to add", + metadata: { noun: NounType.Thing, category: 'example' } + }, + // More items... +], { + forceEmbed: false, + concurrency: 4 // Control the level of parallelism (default: 4) +}) + +// Retrieve a noun +const noun = await db.get(id) + +// Update noun metadata +await db.updateMetadata(id, { + noun: NounType.Thing, + // updated metadata... +}) + +// Delete a noun +await db.delete(id) + +// Search for similar nouns +const results = await db.search(vectorOrText, numResults) +const textResults = await db.searchText("query text", numResults) + +// Search by noun type +const thingNouns = await db.searchByNounTypes([NounType.Thing], numResults) +``` + +### Working with Verbs (Relationships) + +```typescript +// Add a relationship between nouns +await db.addVerb(sourceId, targetId, { + verb: VerbType.RelatedTo, + // other metadata... +}) + +// Get all relationships +const verbs = await db.getAllVerbs() + +// Get relationships by source noun +const outgoingVerbs = await db.getVerbsBySource(sourceId) + +// Get relationships by target noun +const incomingVerbs = await db.getVerbsByTarget(targetId) + +// Get relationships by type +const containsVerbs = await db.getVerbsByType(VerbType.Contains) + +// Get a specific relationship +const verb = await db.getVerb(verbId) + +// Delete a relationship +await db.deleteVerb(verbId) +``` + +## Advanced Configuration + +### Embedding + +```typescript +import { + BrainyData, + createTensorFlowEmbeddingFunction, + createThreadedEmbeddingFunction +} from '@soulcraft/brainy' + +// Use the standard TensorFlow Universal Sentence Encoder embedding function +const db = new BrainyData({ + embeddingFunction: createTensorFlowEmbeddingFunction() +}) +await db.init() + +// Or use the threaded embedding function for better performance +const threadedDb = new BrainyData({ + embeddingFunction: createThreadedEmbeddingFunction({ + fallbackToMain: true // Fall back to main thread if threading fails + }) +}) +await threadedDb.init() + +// Directly embed text to vectors +const vector = await db.embed("Some text to convert to a vector") +``` + +The threaded embedding function runs in a separate thread (Web Worker in browsers, Worker Thread in Node.js) to improve +performance, especially for CPU-intensive embedding operations. It automatically falls back to the main thread if +threading is not available in the current environment. + +### Performance Tuning + +Brainy now includes comprehensive multithreading support to improve performance across all environments: + +1. **Parallel Batch Processing**: Add multiple items concurrently with controlled parallelism +2. **Multithreaded Vector Search**: Perform distance calculations in parallel for faster search operations +3. **Threaded Embedding Generation**: Generate embeddings in separate threads to avoid blocking the main thread +4. **Automatic Environment Detection**: Adapts to browser (Web Workers) and Node.js (Worker Threads) environments + +```typescript +import { BrainyData, euclideanDistance } from '@soulcraft/brainy' + +// Configure with custom options +const db = new BrainyData({ + // Use Euclidean distance instead of default cosine distance + distanceFunction: euclideanDistance, + + // HNSW index configuration for search performance + hnsw: { + M: 16, // Max connections per noun + efConstruction: 200, // Construction candidate list size + efSearch: 50, // Search candidate list size + }, + + // Multithreading options + threading: { + useParallelization: true, // Enable multithreaded search operations + }, + + // Noun and Verb type validation + typeValidation: { + enforceNounTypes: true, // Validate noun types against NounType enum + enforceVerbTypes: true, // Validate verb types against VerbType enum + }, + + // Storage configuration + storage: { + requestPersistentStorage: true, + // Example configuration for cloud storage (replace with your own values): + // s3Storage: { + // bucketName: 'your-s3-bucket-name', + // region: 'your-aws-region' + // // Credentials should be provided via environment variables + // // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY + // } + } +}) +``` + +### Optimized HNSW for Large Datasets + +Brainy includes an optimized HNSW index implementation for large datasets that may not fit entirely in memory, using a +hybrid approach: + +1. **Product Quantization** - Reduces vector dimensionality while preserving similarity relationships +2. **Disk-Based Storage** - Offloads vectors to disk when memory usage exceeds a threshold +3. **Memory-Efficient Indexing** - Optimizes memory usage for large-scale vector collections + +```typescript +import { BrainyData } from '@soulcraft/brainy' + +// Configure with optimized HNSW index for large datasets +const db = new BrainyData({ + hnswOptimized: { + // Standard HNSW parameters + M: 16, // Max connections per noun + efConstruction: 200, // Construction candidate list size + efSearch: 50, // Search candidate list size + + // Memory threshold in bytes - when exceeded, will use disk-based approach + memoryThreshold: 1024 * 1024 * 1024, // 1GB default threshold + + // Product quantization settings for dimensionality reduction + productQuantization: { + enabled: true, // Enable product quantization + numSubvectors: 16, // Number of subvectors to split the vector into + numCentroids: 256 // Number of centroids per subvector + }, + + // Whether to use disk-based storage for the index + useDiskBasedIndex: true // Enable disk-based storage + }, + + // Storage configuration (required for disk-based index) + storage: { + requestPersistentStorage: true + } +}) + +// The optimized index automatically adapts based on dataset size: +// 1. For small datasets: Uses standard in-memory approach +// 2. For medium datasets: Applies product quantization to reduce memory usage +// 3. For large datasets: Combines product quantization with disk-based storage + +// Check status to see memory usage and optimization details +const status = await db.status() +console.log(status.details.index) +``` + +## Distance Functions + +- `cosineDistance` (default) +- `euclideanDistance` +- `manhattanDistance` +- `dotProductDistance` + +## Backup and Restore + +Brainy provides backup and restore capabilities that allow you to: + +- Back up your data +- Transfer data between Brainy instances +- Restore existing data into Brainy for vectorization and indexing +- Backup data for analysis or visualization in other tools + +### Backing Up Data + +```typescript +// Backup all data from the database +const backupData = await db.backup() + +// The backup data includes: +// - All nouns (entities) with their vectors and metadata +// - All verbs (relationships) between nouns +// - Noun types and verb types +// - HNSW index data for fast similarity search +// - Version information + +// Save the backup data to a file (Node.js environment) +import fs from 'fs' + +fs.writeFileSync('brainy-backup.json', JSON.stringify(backupData, null, 2)) +``` + +### Restoring Data + +Brainy's restore functionality can handle: + +1. Complete backups with vectors and index data +2. Sparse data without vectors (vectors will be created during restore) +3. Data without HNSW index (index will be reconstructed if needed) + +```typescript +// Restore data with all options +const restoreResult = await db.restore(backupData, { + clearExisting: true // Whether to clear existing data before restore +}) + +// Import sparse data (without vectors) +// Vectors will be automatically created using the embedding function +const sparseData = { + nouns: [ + { + id: '123', + // No vector field - will be created during import + metadata: { + noun: 'Thing', + text: 'This text will be used to generate a vector' + } + } + ], + verbs: [], + version: '1.0.0' +} + +const sparseImportResult = await db.importSparseData(sparseData) +``` + +### CLI Backup/Restore + +```bash +# Backup data to a file +brainy backup --output brainy-backup.json + +# Restore data from a file +brainy restore --input brainy-backup.json --clear-existing + +# Import sparse data (without vectors) +brainy import-sparse --input sparse-data.json +``` + +## Embedding + +Brainy uses the following embedding approach: + +- TensorFlow Universal Sentence Encoder (high-quality text embeddings) +- Custom embedding functions can be plugged in for specialized domains + +## Extensions + +Brainy includes an augmentation system for extending functionality: + +- **Memory Augmentations**: Different storage backends +- **Sense Augmentations**: Process raw data +- **Cognition Augmentations**: Reasoning and inference +- **Dialog Augmentations**: Text processing and interaction +- **Perception Augmentations**: Data interpretation and visualization +- **Activation Augmentations**: Trigger actions + +### Simplified Augmentation System + +Brainy provides a simplified factory system for creating, importing, and executing augmentations with minimal +boilerplate: + +```typescript +import { + createMemoryAugmentation, + createConduitAugmentation, + createSenseAugmentation, + addWebSocketSupport, + executeStreamlined, + processStaticData, + processStreamingData, + createPipeline +} from '@soulcraft/brainy' + +// Create a memory augmentation with minimal code +const memoryAug = createMemoryAugmentation({ + name: 'simple-memory', + description: 'A simple in-memory storage augmentation', + autoRegister: true, + autoInitialize: true, + + // Implement only the methods you need + storeData: async (key, data) => { + // Your implementation here + return { + success: true, + data: true + } + }, + + retrieveData: async (key) => { + // Your implementation here + return { + success: true, + data: { example: 'data', key } + } + } +}) + +// Add WebSocket support to any augmentation +const wsAugmentation = addWebSocketSupport(memoryAug, { + connectWebSocket: async (url) => { + // Your implementation here + return { + connectionId: 'ws-1', + url, + status: 'connected' + } + } +}) + +// Process static data through a pipeline +const result = await processStaticData( + 'Input data', + [ + { + augmentation: senseAug, + method: 'processRawData', + transformArgs: (data) => [data, 'text'] + }, + { + augmentation: memoryAug, + method: 'storeData', + transformArgs: (data) => ['processed-data', data] + } + ] +) + +// Create a reusable pipeline +const pipeline = createPipeline([ + { + augmentation: senseAug, + method: 'processRawData', + transformArgs: (data) => [data, 'text'] + }, + { + augmentation: memoryAug, + method: 'storeData', + transformArgs: (data) => ['processed-data', data] + } +]) + +// Use the pipeline +const result = await pipeline('New input data') + +// Dynamically load augmentations at runtime +const loadedAugmentations = await loadAugmentationModule( + import('./my-augmentations.js'), + { + autoRegister: true, + autoInitialize: true + } +) +``` + +The simplified augmentation system provides: + +1. **Factory Functions** - Create augmentations with minimal boilerplate +2. **WebSocket Support** - Add WebSocket capabilities to any augmentation +3. **Streamlined Pipeline** - Process data through augmentations more efficiently +4. **Dynamic Loading** - Load augmentations at runtime when needed +5. **Static & Streaming Data** - Handle both static and streaming data with the same API + +### Model Control Protocol (MCP) + +Brainy includes a Model Control Protocol (MCP) implementation that allows external models to access Brainy data and use +the augmentation pipeline as tools: + +- **BrainyMCPAdapter**: Provides access to Brainy data through MCP +- **MCPAugmentationToolset**: Exposes the augmentation pipeline as tools +- **BrainyMCPService**: Integrates the adapter and toolset, providing WebSocket and REST server implementations + +Environment compatibility: + +- **BrainyMCPAdapter** and **MCPAugmentationToolset** can run in any environment (browser, Node.js, server) +- **BrainyMCPService** core functionality works in any environment, but server functionality (WebSocket/REST) is in the + cloud-wrapper project + +For detailed documentation and usage examples, see the [MCP documentation](src/mcp/README.md). + +## Cross-Environment Compatibility + +Brainy is designed to run seamlessly in any environment, from browsers to Node.js to serverless functions and +containers. All Brainy data, functions, and augmentations are environment-agnostic, allowing you to use the same code +everywhere. + +### Environment Detection + +Brainy automatically detects the environment it's running in: + +```typescript +import { environment } from '@soulcraft/brainy' + +// Check which environment we're running in +console.log(`Running in ${ + environment.isBrowser ? 'browser' : + environment.isNode ? 'Node.js' : + 'serverless/unknown' +} environment`) +``` + +### Adaptive Storage + +Storage adapters are automatically selected based on the environment: + +- **Browser**: Uses Origin Private File System (OPFS) when available, falls back to in-memory storage +- **Node.js**: Uses file system storage by default, with options for S3-compatible cloud storage +- **Serverless**: Uses in-memory storage with options for cloud persistence +- **Container**: Automatically detects and uses the appropriate storage based on available capabilities + +### Dynamic Imports + +Brainy uses dynamic imports to load environment-specific dependencies only when needed, keeping the bundle size small +and ensuring compatibility across environments. + +### Browser Support + +Works in all modern browsers: + +- Chrome 86+ +- Edge 86+ +- Opera 72+ +- Chrome for Android 86+ + +For browsers without OPFS support, falls back to in-memory storage. + +## Cloud Deployment + +Brainy can be deployed as a standalone web service on various cloud platforms using the included cloud wrapper: + +- **AWS Lambda and API Gateway**: Deploy as a serverless function with API Gateway +- **Google Cloud Run**: Deploy as a containerized service +- **Cloudflare Workers**: Deploy as a serverless function on the edge + +The cloud wrapper provides both RESTful and WebSocket APIs for all Brainy operations, enabling both request-response and +real-time communication patterns. It supports multiple storage backends and can be configured via environment variables. + +Key features of the cloud wrapper: + +- RESTful API for standard CRUD operations +- WebSocket API for real-time updates and subscriptions +- Model Control Protocol (MCP) service for external model access +- Support for multiple storage backends (Memory, FileSystem, S3) +- Configurable via environment variables +- Deployment scripts for AWS, Google Cloud, and Cloudflare + +### Deploying to the Cloud + +You can deploy the cloud wrapper to various cloud platforms using the following npm scripts from the root directory: + +```bash +# Deploy to AWS Lambda and API Gateway +npm run deploy:cloud:aws + +# Deploy to Google Cloud Run +npm run deploy:cloud:gcp + +# Deploy to Cloudflare Workers +npm run deploy:cloud:cloudflare + +# Show available deployment options +npm run deploy:cloud +``` + +Before deploying, make sure to configure the appropriate environment variables in the `cloud-wrapper/.env` file. See +the [Cloud Wrapper README](cloud-wrapper/README.md) for detailed configuration instructions and API documentation. + +## Related Projects + +- **[Cartographer](https://github.com/sodal-project/cartographer)** - A companion project that provides standardized + interfaces for interacting with Brainy + +## Demo + +The repository includes a comprehensive demo that showcases Brainy's main features: + +- `demo/index.html` - A single demo page with animations demonstrating Brainy's features. + - **[Try the live demo](https://soulcraft-research.github.io/brainy/demo/index.html)** - Check out the + interactive demo on + GitHub Pages + - Or run it locally with `npm run demo` (see [demo instructions](README.demo.md) for details) + - To deploy your own version to GitHub Pages, use the GitHub Actions workflow in + `.github/workflows/deploy-demo.yml`, + which automatically deploys when pushing to the main branch or can be manually triggered + - To use a custom domain (like www.soulcraft.com): + 1. A CNAME file is already included in the demo directory + 2. In your GitHub repository settings, go to Pages > Custom domain and enter your domain + 3. Configure your domain's DNS settings to point to GitHub Pages: + - Add a CNAME record for www pointing to `.github.io` (e.g., `soulcraft-research.github.io`) + - Or for an apex domain (soulcraft.com), add A records pointing to GitHub Pages IP addresses + +The demo showcases: + +- How Brainy runs in different environments (browser, Node.js, server, cloud) +- How the noun-verb data model works +- How HNSW search works + +## Syncing Brainy Instances + +You can use the conduit augmentations to sync Brainy instances: + +- **WebSocket iConduit**: For syncing between browsers and servers, or between servers. WebSockets cannot be used for + direct browser-to-browser communication without a server in the middle. +- **WebRTC iConduit**: For direct peer-to-peer syncing between browsers. This is the recommended approach for + browser-to-browser communication. + +#### WebSocket Sync Example + +```typescript +import { + BrainyData, + pipeline, + createConduitAugmentation +} from '@soulcraft/brainy' + +// Create and initialize the database +const db = new BrainyData() +await db.init() + +// Create a WebSocket conduit augmentation +const wsConduit = await createConduitAugmentation('websocket', 'my-websocket-sync') + +// Register the augmentation with the pipeline +pipeline.register(wsConduit) + +// Connect to another Brainy instance (server or browser) +// Replace the example URL below with your actual WebSocket server URL +const connectionResult = await pipeline.executeConduitPipeline( + 'establishConnection', + ['wss://example-websocket-server.com/brainy-sync', { protocols: 'brainy-sync' }] +) + +if (connectionResult[0] && (await connectionResult[0]).success) { + const connection = (await connectionResult[0]).data + + // Read data from the remote instance + const readResult = await pipeline.executeConduitPipeline( + 'readData', + [{ connectionId: connection.connectionId, query: { type: 'getAllNouns' } }] + ) + + // Process and add the received data to the local instance + if (readResult[0] && (await readResult[0]).success) { + const remoteNouns = (await readResult[0]).data + for (const noun of remoteNouns) { + await db.add(noun.vector, noun.metadata) + } + } + + // Set up real-time sync by monitoring the stream + await wsConduit.monitorStream(connection.connectionId, async (data) => { + // Handle incoming data (e.g., new nouns, verbs, updates) + if (data.type === 'newNoun') { + await db.add(data.vector, data.metadata) + } else if (data.type === 'newVerb') { + await db.addVerb(data.sourceId, data.targetId, data.vector, data.options) + } + }) +} +``` + +#### WebRTC Peer-to-Peer Sync Example + +```typescript +import { + BrainyData, + pipeline, + createConduitAugmentation +} from '@soulcraft/brainy' + +// Create and initialize the database +const db = new BrainyData() +await db.init() + +// Create a WebRTC conduit augmentation +const webrtcConduit = await createConduitAugmentation('webrtc', 'my-webrtc-sync') + +// Register the augmentation with the pipeline +pipeline.register(webrtcConduit) + +// Connect to a peer using a signaling server +// Replace the example values below with your actual configuration +const connectionResult = await pipeline.executeConduitPipeline( + 'establishConnection', + [ + 'peer-id-to-connect-to', // Replace with actual peer ID + { + signalServerUrl: 'wss://example-signal-server.com', // Replace with your signal server + localPeerId: 'my-local-peer-id', // Replace with your local peer ID + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // Public STUN server + } + ] +) + +if (connectionResult[0] && (await connectionResult[0]).success) { + const connection = (await connectionResult[0]).data + + // Set up real-time sync by monitoring the stream + await webrtcConduit.monitorStream(connection.connectionId, async (data) => { + // Handle incoming data (e.g., new nouns, verbs, updates) + if (data.type === 'newNoun') { + await db.add(data.vector, data.metadata) + } else if (data.type === 'newVerb') { + await db.addVerb(data.sourceId, data.targetId, data.vector, data.options) + } + }) + + // When adding new data locally, also send to the peer + const nounId = await db.add("New data to sync", { noun: "Thing" }) + + // Send the new noun to the peer + await pipeline.executeConduitPipeline( + 'writeData', + [ + { + connectionId: connection.connectionId, + data: { + type: 'newNoun', + id: nounId, + vector: (await db.get(nounId)).vector, + metadata: (await db.get(nounId)).metadata + } + } + ] + ) +} +``` + +#### Browser-Server Search Example + +Brainy supports searching a server-hosted instance from a browser, storing results locally, and performing further +searches against the local instance: + +```typescript +import { BrainyData } from '@soulcraft/brainy' + +// Create and initialize the database with remote server configuration +// Replace the example URL below with your actual Brainy server URL +const db = new BrainyData({ + remoteServer: { + url: 'wss://example-brainy-server.com/ws', // Replace with your server URL + protocols: 'brainy-sync', + autoConnect: true // Connect automatically during initialization + } +}) +await db.init() + +// Or connect manually after initialization +if (!db.isConnectedToRemoteServer()) { + // Replace the example URL below with your actual Brainy server URL + await db.connectToRemoteServer('wss://example-brainy-server.com/ws', 'brainy-sync') +} + +// Search the remote server (results are stored locally) +const remoteResults = await db.searchText('machine learning', 5, { searchMode: 'remote' }) + +// Search the local database (includes previously stored results) +const localResults = await db.searchText('machine learning', 5, { searchMode: 'local' }) + +// Perform a combined search (local first, then remote if needed) +const combinedResults = await db.searchText('neural networks', 5, { searchMode: 'combined' }) + +// Add data to both local and remote instances +const id = await db.addToBoth('Deep learning is a subset of machine learning', { + noun: 'Concept', + category: 'AI', + tags: ['deep learning', 'neural networks'] +}) + +// Clean up when done +await db.shutDown() +``` + +--- + +## 📈 Scaling Strategy + +Brainy is designed to handle datasets of various sizes, from small collections to large-scale deployments. For +terabyte-scale data that can't fit entirely in memory, we provide several approaches: + +- **Disk-Based HNSW**: Modified implementations using intelligent caching and partial loading +- **Distributed HNSW**: Sharding and partitioning across multiple machines +- **Hybrid Solutions**: Combining quantization techniques with multi-tier architectures + +For detailed information on how to scale Brainy for large datasets, see our +comprehensive [Scaling Strategy](scalingStrategy.md) document. + +## Requirements + +- Node.js >= 23.11.0 + +## Contributing + +For detailed contribution guidelines, please see [CONTRIBUTING.md](CONTRIBUTING.md). + +We have a [Code of Conduct](CODE_OF_CONDUCT.md) that all contributors are expected to follow. + +### Reporting Issues + +We use GitHub issues to track bugs and feature requests. Please use the provided issue templates when creating a new +issue: + +- [Bug Report Template](.github/ISSUE_TEMPLATE/bug_report.md) +- [Feature Request Template](.github/ISSUE_TEMPLATE/feature_request.md) + +### Code Style Guidelines + +Brainy follows a specific code style to maintain consistency throughout the codebase: + +1. **No Semicolons**: All code in the project should avoid using semicolons wherever possible +2. **Formatting**: The project uses Prettier for code formatting +3. **Linting**: ESLint is configured with specific rules for the project +4. **TypeScript Configuration**: Strict type checking enabled with ES2020 target +5. **Commit Messages**: Use the imperative mood and keep the first line concise + +### Development Workflow + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request + +### Badge Maintenance + +The README badges are automatically updated during the build process: + +1. **npm Version Badge**: The npm version badge is automatically updated to match the version in package.json when: + - Running `npm run build` (via the prebuild script) + - Running `npm version` commands (patch, minor, major) + - Manually running `node scripts/generate-version.js` + +This ensures that the badge always reflects the current version in package.json, even before publishing to npm. + +## License + +[MIT](LICENSE) diff --git a/brainy.png b/brainy.png new file mode 100644 index 0000000000000000000000000000000000000000..01dfc90136956756e43ddc0e9c096f1219ce694c GIT binary patch literal 222935 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 { + process.exit(code) +}) diff --git a/cloud-wrapper/.env.example b/cloud-wrapper/.env.example new file mode 100644 index 00000000..c7614644 --- /dev/null +++ b/cloud-wrapper/.env.example @@ -0,0 +1,24 @@ +# Server configuration +PORT=3000 + +# Storage configuration +# Options: 'filesystem', 'memory', 's3' +STORAGE_TYPE=filesystem +STORAGE_ROOT_DIR=./data + +# S3 Storage configuration (only used if STORAGE_TYPE=s3) +S3_BUCKET_NAME=your-bucket-name +S3_ACCESS_KEY_ID=your-access-key +S3_SECRET_ACCESS_KEY=your-secret-key +S3_REGION=us-east-1 +# Optional: For custom S3-compatible services like MinIO, DigitalOcean Spaces, etc. +# S3_ENDPOINT=https://your-custom-endpoint + +# Embedding configuration +# Set to 'true' to use simple embedding (faster but less accurate) +USE_SIMPLE_EMBEDDING=false + +# HNSW index configuration (optional) +# HNSW_M=16 # Max connections per node +# HNSW_EF_CONSTRUCTION=200 # Construction candidate list size +# HNSW_EF_SEARCH=50 # Search candidate list size diff --git a/cloud-wrapper/README.md b/cloud-wrapper/README.md new file mode 100644 index 00000000..436691cb --- /dev/null +++ b/cloud-wrapper/README.md @@ -0,0 +1,341 @@ +

+ +A standalone web service wrapper for the [Brainy](https://github.com/soulcraft/brainy) vector graph database. This wrapper allows you to deploy Brainy as a RESTful API service on various cloud platforms including AWS, Google Cloud, and Cloudflare. + +## Features + +- RESTful API for all Brainy operations +- WebSocket API for real-time updates and subscriptions +- Model Control Protocol (MCP) service for external model access +- Support for multiple storage backends (Memory, FileSystem, S3) +- Configurable via environment variables +- Deployment scripts for AWS, Google Cloud, and Cloudflare +- Secure by default with Helmet middleware +- Cross-origin resource sharing (CORS) support + +## Prerequisites + +- Node.js 23.11.0 or higher +- npm or yarn +- For cloud deployments: + - AWS: AWS CLI installed and configured + - Google Cloud: Google Cloud SDK installed and configured + - Cloudflare: Wrangler CLI installed and configured + +## Installation + +1. Clone the repository: + ```bash + git clone https://github.com/soulcraft/brainy.git + cd brainy/cloud-wrapper + ``` + +2. Install dependencies: + ```bash + npm install --legacy-peer-deps + ``` + +3. Create a `.env` file based on the example: + ```bash + cp .env.example .env + ``` + +4. Edit the `.env` file to configure your environment. + +## Configuration + +The cloud wrapper can be configured using environment variables. See the `.env.example` file for available options. + +### Storage Options + +- `STORAGE_TYPE`: The type of storage to use. Options: + - `memory`: In-memory storage (default for Cloudflare) + - `filesystem`: File system storage (default for local and AWS/GCP) + - `s3`: S3-compatible storage (AWS S3, MinIO, etc.) + - `r2`: Cloudflare R2 storage (Cloudflare only) + +### S3 Storage Configuration + +When using `STORAGE_TYPE=s3`, the following environment variables are required: + +- `S3_BUCKET_NAME`: The name of the S3 bucket +- `S3_ACCESS_KEY_ID`: Your S3 access key ID +- `S3_SECRET_ACCESS_KEY`: Your S3 secret access key +- `S3_REGION`: The S3 region (default: `us-east-1`) +- `S3_ENDPOINT` (optional): Custom endpoint for S3-compatible services + +### MCP Service Configuration + +The Model Control Protocol (MCP) service can be configured using the following environment variables: + +- `MCP_WS_PORT`: Port for the MCP WebSocket server (if not set, WebSocket server is disabled) +- `MCP_REST_PORT`: Port for the MCP REST server (if not set, REST server is disabled) +- `MCP_ENABLE_AUTH`: Enable authentication for MCP requests (`true` or `false`) +- `MCP_API_KEYS`: Comma-separated list of API keys for authentication +- `MCP_RATE_LIMIT_REQUESTS`: Maximum number of requests per time window +- `MCP_RATE_LIMIT_WINDOW_MS`: Time window for rate limiting in milliseconds (default: `60000`) +- `MCP_ENABLE_CORS`: Enable CORS for MCP REST server (`true` or `false`) + +## Local Development + +1. Build the project: + ```bash + npm run build + ``` + +2. Start the development server: + ```bash + npm run dev + ``` + +3. The API will be available at `http://localhost:3000`. + +## API Endpoints + +Brainy Cloud Wrapper provides REST API, WebSocket API, and Model Control Protocol (MCP) API for interacting with the database. + +### REST API + +#### Status + +- `GET /api/status`: Get database status + +#### Nouns (Entities) + +- `POST /api/nouns`: Add a new noun + - Body: `{ "text": "Your text", "metadata": { ... } }` +- `GET /api/nouns/:id`: Get a noun by ID +- `PUT /api/nouns/:id`: Update noun metadata + - Body: `{ "metadata": { ... } }` +- `DELETE /api/nouns/:id`: Delete a noun + +#### Search + +- `POST /api/search`: Search for similar nouns + - Body: `{ "query": "Your search query", "limit": 10 }` + +#### Verbs (Relationships) + +- `POST /api/verbs`: Add a relationship between nouns + - Body: `{ "sourceId": "...", "targetId": "...", "metadata": { ... } }` +- `GET /api/verbs`: Get all relationships +- `GET /api/verbs/source/:id`: Get relationships by source +- `GET /api/verbs/target/:id`: Get relationships by target +- `DELETE /api/verbs/:id`: Delete a relationship + +#### Database Management + +- `DELETE /api/clear`: Clear all data + +### WebSocket API + +The WebSocket API provides real-time communication with the Brainy database. Connect to the WebSocket server at `ws://your-server:port`. + +#### Message Format + +All WebSocket messages follow this format: + +```json +{ + "type": "messageType", + "id": "unique-message-id", + "payload": { + // Message-specific data + } +} +``` + +#### Available Message Types + +##### Status +- Request: `{ "type": "status" }` +- Response: `{ "type": "status", "id": "...", "payload": { /* database status */ } }` + +##### Nouns (Entities) +- Add a noun: + - Request: `{ "type": "addNoun", "payload": { "text": "Your text", "metadata": { ... } } }` + - Response: `{ "type": "addNoun", "id": "...", "payload": { "id": "new-noun-id" } }` + +- Get a noun: + - Request: `{ "type": "getNoun", "payload": { "id": "noun-id" } }` + - Response: `{ "type": "getNoun", "id": "...", "payload": { /* noun data */ } }` + +- Update a noun: + - Request: `{ "type": "updateNoun", "payload": { "id": "noun-id", "metadata": { ... } } }` + - Response: `{ "type": "updateNoun", "id": "...", "payload": { "success": true } }` + +- Delete a noun: + - Request: `{ "type": "deleteNoun", "payload": { "id": "noun-id" } }` + - Response: `{ "type": "deleteNoun", "id": "...", "payload": { "success": true } }` + +##### Search +- Search for similar nouns: + - Request: `{ "type": "search", "payload": { "query": "Your search query", "limit": 10 } }` + - Response: `{ "type": "search", "id": "...", "payload": [ /* search results */ ] }` + +##### Verbs (Relationships) +- Add a verb: + - Request: `{ "type": "addVerb", "payload": { "sourceId": "...", "targetId": "...", "metadata": { ... } } }` + - Response: `{ "type": "addVerb", "id": "...", "payload": { "success": true } }` + +- Get all verbs: + - Request: `{ "type": "getVerbs" }` + - Response: `{ "type": "getVerbs", "id": "...", "payload": [ /* all verbs */ ] }` + +- Get verbs by source: + - Request: `{ "type": "getVerbsBySource", "payload": { "id": "source-id" } }` + - Response: `{ "type": "getVerbsBySource", "id": "...", "payload": [ /* verbs */ ] }` + +- Get verbs by target: + - Request: `{ "type": "getVerbsByTarget", "payload": { "id": "target-id" } }` + - Response: `{ "type": "getVerbsByTarget", "id": "...", "payload": [ /* verbs */ ] }` + +- Delete a verb: + - Request: `{ "type": "deleteVerb", "payload": { "id": "verb-id" } }` + - Response: `{ "type": "deleteVerb", "id": "...", "payload": { "success": true } }` + +##### Database Management +- Clear all data: + - Request: `{ "type": "clear" }` + - Response: `{ "type": "clear", "id": "...", "payload": { "success": true } }` + +#### Real-time Subscriptions + +The WebSocket API supports subscribing to real-time updates: + +- Subscribe to updates: + - Request: `{ "type": "subscribe", "payload": { "type": "nouns" } }` + - Response: `{ "type": "subscribe", "id": "...", "payload": { "success": true, "type": "nouns" } }` + +- Unsubscribe from updates: + - Request: `{ "type": "unsubscribe", "payload": { "type": "nouns" } }` + - Response: `{ "type": "unsubscribe", "id": "...", "payload": { "success": true, "type": "nouns" } }` + +Available subscription types: +- `nouns`: Updates about nouns (added, updated, deleted) +- `verbs`: Updates about verbs (added, deleted) +- `searchResults`: Updates about search results + +When subscribed, you'll receive messages when relevant events occur: + +```json +{ + "type": "subscribe", + "payload": { + "type": "nouns", + "data": { + "type": "added", + "id": "noun-id", + "data": { /* noun data */ } + } + } +} +``` + +### MCP API + +The Model Control Protocol (MCP) API provides a standardized interface for external models to access Brainy data and use the augmentation pipeline as tools. The MCP API is available through both WebSocket and REST endpoints. + +#### MCP REST API Endpoints + +- `POST /mcp/data`: Access Brainy data (search, get, add, etc.) +- `POST /mcp/tools`: Execute augmentation pipeline tools +- `POST /mcp/system`: Get system information +- `POST /mcp/auth`: Authenticate with the MCP service +- `GET /mcp/tools`: Get available tools + +#### MCP WebSocket + +Connect to the MCP WebSocket server at `ws://your-server:MCP_WS_PORT` and send JSON messages in the MCP format: + +```json +{ + "type": "DATA_ACCESS", + "requestId": "unique-request-id", + "version": "1.0", + "operation": "search", + "parameters": { + "query": "Your search query", + "k": 5 + } +} +``` + +For detailed documentation on the MCP API, see the [MCP documentation](../src/mcp/README.md). + +## Cloud Deployment + +### AWS Lambda and API Gateway + +1. Configure AWS-specific environment variables: + ``` + AWS_REGION=us-east-1 + AWS_FUNCTION_NAME=brainy-cloud-service + AWS_API_GATEWAY_NAME=brainy-api + AWS_STAGE_NAME=prod + AWS_ACCOUNT_ID=your-account-id + ``` + +2. Deploy to AWS: + ```bash + npm run deploy:aws + ``` + +### Google Cloud Run + +1. Configure GCP-specific environment variables: + ``` + GCP_PROJECT_ID=your-project-id + GCP_REGION=us-central1 + GCP_SERVICE_NAME=brainy-cloud-service + GCP_IMAGE_NAME=brainy-cloud-service + GCP_MEMORY=512Mi + GCP_CPU=1 + GCP_MAX_INSTANCES=10 + GCP_MIN_INSTANCES=0 + ``` + +2. Deploy to Google Cloud: + ```bash + npm run deploy:gcp + ``` + +### Cloudflare Workers + +1. Configure Cloudflare-specific environment variables: + ``` + CF_ACCOUNT_ID=your-account-id + CF_WORKER_NAME=brainy-cloud-service + CF_KV_NAMESPACE=BRAINY_STORAGE + CF_R2_BUCKET=brainy-storage + ``` + +2. Deploy to Cloudflare: + ```bash + npm run deploy:cloudflare + ``` + +## Storage Considerations + +### AWS Lambda + +When deploying to AWS Lambda, it's recommended to use S3 storage for persistence. The filesystem storage option will work but data will be lost when the Lambda function is recycled. + +### Google Cloud Run + +For Google Cloud Run, you can use either filesystem storage (for ephemeral storage) or S3-compatible storage (like Google Cloud Storage with an S3 compatibility layer). + +### Cloudflare Workers + +Cloudflare Workers have limited storage options. The recommended approach is to use: +- Cloudflare KV for small datasets +- Cloudflare R2 for larger datasets +- Memory storage for temporary data + +## License + +MIT diff --git a/cloud-wrapper/package-lock.json b/cloud-wrapper/package-lock.json new file mode 100644 index 00000000..be1b9252 --- /dev/null +++ b/cloud-wrapper/package-lock.json @@ -0,0 +1,4082 @@ +{ + "name": "@soulcraft/brainy-cloud", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@soulcraft/brainy-cloud", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@soulcraft/brainy": "^0.9.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "uuid": "^9.0.1", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", + "@types/node": "^20.10.5", + "@types/uuid": "^9.0.7", + "@types/ws": "^8.5.10", + "nodemon": "^3.0.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=23.11.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.832.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.832.0.tgz", + "integrity": "sha512-S+md1zCe71SEuaRDuLHq4mzhYYkVxR1ENa8NwrgInfYoC4xo8/pESoR6i0ZZpcLs0Jw4EyVInWYs4GgDHW70qQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/credential-provider-node": "3.830.0", + "@aws-sdk/middleware-bucket-endpoint": "3.830.0", + "@aws-sdk/middleware-expect-continue": "3.821.0", + "@aws-sdk/middleware-flexible-checksums": "3.826.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-location-constraint": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-sdk-s3": "3.826.0", + "@aws-sdk/middleware-ssec": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.828.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/signature-v4-multi-region": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.828.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/eventstream-serde-browser": "^4.0.4", + "@smithy/eventstream-serde-config-resolver": "^4.1.2", + "@smithy/eventstream-serde-node": "^4.0.4", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-blob-browser": "^4.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/hash-stream-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/md5-js": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-retry": "^4.1.12", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.19", + "@smithy/util-defaults-mode-node": "^4.0.19", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.5", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.830.0.tgz", + "integrity": "sha512-5zCEpfI+zwX2SIa258L+TItNbBoAvQQ6w74qdFM6YJufQ1F9tvwjTX8T+eSTT9nsFIvfYnUaGalWwJVfmJUgVQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.828.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.828.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-retry": "^4.1.12", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.19", + "@smithy/util-defaults-mode-node": "^4.0.19", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.826.0.tgz", + "integrity": "sha512-BGbQYzWj3ps+dblq33FY5tz/SsgJCcXX0zjQlSC07tYvU1jHTUvsefphyig+fY38xZ4wdKjbTop+KUmXUYrOXw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.5.3", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.826.0.tgz", + "integrity": "sha512-DK3pQY8+iKK3MGDdC3uOZQ2psU01obaKlTYhEwNu4VWzgwQL4Vi3sWj4xSWGEK41vqZxiRLq6fOq7ysRI+qEZA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.826.0.tgz", + "integrity": "sha512-N+IVZBh+yx/9GbMZTKO/gErBi/FYZQtcFRItoLbY+6WU+0cSWyZYfkoeOxHmQV3iX9k65oljERIWUmL9x6OSQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.830.0.tgz", + "integrity": "sha512-zeQenzvh8JRY5nULd8izdjVGoCM1tgsVVsrLSwDkHxZTTW0hW/bmOmXfvdaE0wDdomXW7m2CkQDSmP7XdvNXZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/credential-provider-env": "3.826.0", + "@aws-sdk/credential-provider-http": "3.826.0", + "@aws-sdk/credential-provider-process": "3.826.0", + "@aws-sdk/credential-provider-sso": "3.830.0", + "@aws-sdk/credential-provider-web-identity": "3.830.0", + "@aws-sdk/nested-clients": "3.830.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.830.0.tgz", + "integrity": "sha512-X/2LrTgwtK1pkWrvofxQBI8VTi6QVLtSMpsKKPPnJQ0vgqC0e4czSIs3ZxiEsOkCBaQ2usXSiKyh0ccsQ6k2OA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.826.0", + "@aws-sdk/credential-provider-http": "3.826.0", + "@aws-sdk/credential-provider-ini": "3.830.0", + "@aws-sdk/credential-provider-process": "3.826.0", + "@aws-sdk/credential-provider-sso": "3.830.0", + "@aws-sdk/credential-provider-web-identity": "3.830.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.826.0.tgz", + "integrity": "sha512-kURrc4amu3NLtw1yZw7EoLNEVhmOMRUTs+chaNcmS+ERm3yK0nKjaJzmKahmwlTQTSl3wJ8jjK7x962VPo+zWw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.830.0.tgz", + "integrity": "sha512-+VdRpZmfekzpySqZikAKx6l5ndnLGluioIgUG4ZznrButgFD/iogzFtGmBDFB3ZLViX1l4pMXru0zFwJEZT21Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.830.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/token-providers": "3.830.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.830.0.tgz", + "integrity": "sha512-hPYrKsZeeOdLROJ59T6Y8yZ0iwC/60L3qhZXjapBFjbqBtMaQiMTI645K6xVXBioA6vxXq7B4aLOhYqk6Fy/Ww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/nested-clients": "3.830.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.830.0.tgz", + "integrity": "sha512-ElVeCReZSH5Ds+/pkL5ebneJjuo8f49e9JXV1cYizuH0OAOQfYaBU9+M+7+rn61pTttOFE8W//qKzrXBBJhfMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.821.0.tgz", + "integrity": "sha512-zAOoSZKe1njOrtynvK6ZORU57YGv5I7KP4+rwOvUN3ZhJbQ7QPf8gKtFUCYAPRMegaXCKF/ADPtDZBAmM+zZ9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.826.0.tgz", + "integrity": "sha512-Fz9w8CFYPfSlHEB6feSsi06hdS+s+FB8k5pO4L7IV0tUa78mlhxF/VNlAJaVWYyOkZXl4HPH2K48aapACSQOXw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", + "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.821.0.tgz", + "integrity": "sha512-sKrm80k0t3R0on8aA/WhWFoMaAl4yvdk+riotmMElLUpcMcRXAd1+600uFVrxJqZdbrKQ0mjX0PjT68DlkYXLg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", + "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", + "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.826.0.tgz", + "integrity": "sha512-8F0qWaYKfvD/de1AKccXuigM+gb/IZSncCqxdnFWqd+TFzo9qI9Hh+TpUhWOMYSgxsMsYQ8ipmLzlD/lDhjrmA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/core": "^3.5.3", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.821.0.tgz", + "integrity": "sha512-YYi1Hhr2AYiU/24cQc8HIB+SWbQo6FBkMYojVuz/zgrtkFmALxENGF/21OPg7f/QWd+eadZJRxCjmRwh5F2Cxg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.828.0.tgz", + "integrity": "sha512-nixvI/SETXRdmrVab4D9LvXT3lrXkwAWGWk2GVvQvzlqN1/M/RfClj+o37Sn4FqRkGH9o9g7Fqb1YqZ4mqDAtA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@smithy/core": "^3.5.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.830.0.tgz", + "integrity": "sha512-5N5YTlBr1vtxf7+t+UaIQ625KEAmm7fY9o1e3MgGOi/paBoI0+axr3ud24qLIy0NSzFlAHEaxUSWxcERNjIoZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.828.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.828.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-retry": "^4.1.12", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.19", + "@smithy/util-defaults-mode-node": "^4.0.19", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", + "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.826.0.tgz", + "integrity": "sha512-3fEi/zy6tpMzomYosksGtu7jZqGFcdBXoL7YRsG7OEeQzBbOW9B+fVaQZ4jnsViSjzA/yKydLahMrfPnt+iaxg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.830.0.tgz", + "integrity": "sha512-aJ4guFwj92nV9D+EgJPaCFKK0I3y2uMchiDfh69Zqnmwfxxxfxat6F79VA7PS0BdbjRfhLbn+Ghjftnomu2c1g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/nested-clients": "3.830.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.821.0.tgz", + "integrity": "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", + "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.828.0.tgz", + "integrity": "sha512-RvKch111SblqdkPzg3oCIdlGxlQs+k+P7Etory9FmxPHyPDvsP1j1c74PmgYqtzzMWmoXTjd+c9naUHh9xG8xg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", + "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.828.0.tgz", + "integrity": "sha512-LdN6fTBzTlQmc8O8f1wiZN0qF3yBWVGis7NwpWK7FUEzP9bEZRxYfIkV9oV9zpt6iNRze1SedK3JQVB/udxBoA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.828.0", + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", + "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", + "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.4.tgz", + "integrity": "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.4.tgz", + "integrity": "sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.2.tgz", + "integrity": "sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.4.tgz", + "integrity": "sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.4.tgz", + "integrity": "sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", + "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.4.tgz", + "integrity": "sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^4.0.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.4.tgz", + "integrity": "sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.4.tgz", + "integrity": "sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.11.tgz", + "integrity": "sha512-zDogwtRLzKl58lVS8wPcARevFZNBOOqnmzWWxVe9XiaXU2CADFjvJ9XfNibgkOWs08sxLuSr81NrpY4mgp9OwQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.12.tgz", + "integrity": "sha512-wvIH70c4e91NtRxdaLZF+mbLZ/HcC6yg7ySKUiufL6ESp6zJUSnJucZ309AvG9nqCFHSRB5I6T3Ez1Q9wCh0Ww==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.5", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.5.tgz", + "integrity": "sha512-LvcfhrnCBvCmTee81pRlh1F39yTS/+kYleVeLCwNtkY8wtGg8V/ca9rbZZvYIl8OjlMtL6KIjaiL/lgVqHD2nA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.3.tgz", + "integrity": "sha512-xxzNYgA0HD6ETCe5QJubsxP0hQH3QK3kbpJz3QrosBCuIWyEXLR/CO5hFb2OeawEKUxMNhz3a1nuJNN2np2RMA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.19.tgz", + "integrity": "sha512-mvLMh87xSmQrV5XqnUYEPoiFFeEGYeAKIDDKdhE2ahqitm8OHM3aSvhqL6rrK6wm1brIk90JhxDf5lf2hbrLbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.19.tgz", + "integrity": "sha512-8tYnx+LUfj6m+zkUUIrIQJxPM1xVxfRBvoGHua7R/i6qAxOMjqR6CpEpDwKoIs1o0+hOjGvkKE23CafKL0vJ9w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.5.tgz", + "integrity": "sha512-V7MSjVDTlEt/plmOFBn1762Dyu5uqMrV2Pl2X0dYk4XvWfdWJNe9Bs5Bzb56wkCuiWjSfClVMGcsuKrGj7S/yg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.5", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", + "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.5.tgz", + "integrity": "sha512-4QvC49HTteI1gfemu0I1syWovJgPvGn7CVUoN9ZFkdvr/cCFkrEL7qNCdx/2eICqDWEGnnr68oMdSIPCLAriSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@soulcraft/brainy": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@soulcraft/brainy/-/brainy-0.9.2.tgz", + "integrity": "sha512-xIDpurMOv7vWRpeFP9GTDtYMGDHJsJ+XlysvbCM5aeAXu1ajNOToTU+ghsPIN3UYaWXn1FBBcoBfYgMtJh4vmw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-s3": "^3.427.0", + "@tensorflow-models/universal-sentence-encoder": "^1.3.3", + "@tensorflow/tfjs": "^4.22.0", + "@tensorflow/tfjs-backend-cpu": "^4.22.0", + "@tensorflow/tfjs-converter": "^4.22.0", + "@tensorflow/tfjs-core": "^4.22.0", + "@tensorflow/tfjs-layers": "^4.22.0", + "buffer": "^6.0.3", + "commander": "^14.0.0", + "omelette": "^0.4.17", + "uuid": "^9.0.0" + }, + "bin": { + "brainy": "cli-wrapper.js" + }, + "engines": { + "node": ">=23.11.0" + } + }, + "node_modules/@soulcraft/brainy/node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", + "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", + "license": "Apache-2.0", + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@soulcraft/brainy/node_modules/@tensorflow/tfjs-converter": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", + "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@soulcraft/brainy/node_modules/@tensorflow/tfjs-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", + "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.7.0", + "@types/seedrandom": "^2.4.28", + "@webgpu/types": "0.1.38", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@soulcraft/brainy/node_modules/@tensorflow/tfjs-layers": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz", + "integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==", + "license": "Apache-2.0 AND MIT", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@soulcraft/brainy/node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@soulcraft/brainy/node_modules/@webgpu/types": { + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", + "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", + "license": "BSD-3-Clause" + }, + "node_modules/@tensorflow-models/universal-sentence-encoder": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@tensorflow-models/universal-sentence-encoder/-/universal-sentence-encoder-1.3.3.tgz", + "integrity": "sha512-mipL7ad0CW6uQ68FUkNgkNj/zgA4qgBnNcnMMkNTdL9MUMnzCxu3AE8pWnx2ReKHwdqEG4e8IpaYKfH4B8bojg==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-converter": "^3.6.0", + "@tensorflow/tfjs-core": "^3.6.0" + } + }, + "node_modules/@tensorflow/tfjs": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.22.0.tgz", + "integrity": "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@tensorflow/tfjs-backend-webgl": "4.22.0", + "@tensorflow/tfjs-converter": "4.22.0", + "@tensorflow/tfjs-core": "4.22.0", + "@tensorflow/tfjs-data": "4.22.0", + "@tensorflow/tfjs-layers": "4.22.0", + "argparse": "^1.0.10", + "chalk": "^4.1.0", + "core-js": "3.29.1", + "regenerator-runtime": "^0.13.5", + "yargs": "^16.0.3" + }, + "bin": { + "tfjs-custom-module": "dist/tools/custom_module/cli.js" + } + }, + "node_modules/@tensorflow/tfjs-converter": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.21.0.tgz", + "integrity": "sha512-12Y4zVDq3yW+wSjSDpSv4HnpL2sDZrNiGSg8XNiDE4HQBdjdA+a+Q3sZF/8NV9y2yoBhL5L7V4mMLDdbZBd9/Q==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@tensorflow/tfjs-core": "3.21.0" + } + }, + "node_modules/@tensorflow/tfjs-core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.21.0.tgz", + "integrity": "sha512-YSfsswOqWfd+M4bXIhT3hwtAb+IV8+ODwIxwdFR/7jTAPZP1wMVnSlpKnXHAN64HFOiP+Tm3HmKusEZ0+09A0w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "@types/webgl-ext": "0.0.30", + "@webgpu/types": "0.1.16", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", + "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", + "license": "Apache-2.0", + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-backend-webgl": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", + "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-converter": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", + "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", + "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.7.0", + "@types/seedrandom": "^2.4.28", + "@webgpu/types": "0.1.38", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-data": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz", + "integrity": "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==", + "license": "Apache-2.0", + "dependencies": { + "@types/node-fetch": "^2.1.2", + "node-fetch": "~2.6.1", + "string_decoder": "^1.3.0" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0", + "seedrandom": "^3.0.5" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/@tensorflow/tfjs-layers": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz", + "integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==", + "license": "Apache-2.0 AND MIT", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/@webgpu/types": { + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", + "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", + "license": "BSD-3-Clause" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.3.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", + "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/seedrandom": { + "version": "2.4.34", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", + "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "node_modules/@types/webgl-ext": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", + "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz", + "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", + "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/omelette": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/omelette/-/omelette-0.4.17.tgz", + "integrity": "sha512-UlU69G6Bhu0XFjw3tjFZ0qyiMUjAOR+rdzblA1nLQ8xiqFtxOVlkhM39BlgTpLFx9fxkm6rnxNNRsS5GxE/yww==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/cloud-wrapper/package.json b/cloud-wrapper/package.json new file mode 100644 index 00000000..80ff4b20 --- /dev/null +++ b/cloud-wrapper/package.json @@ -0,0 +1,92 @@ +{ + "name": "@soulcraft/brainy-cloud", + "version": "0.1.0", + "description": "Cloud deployment wrapper for Brainy vector graph database", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "nodemon --exec node --experimental-specifier-resolution=node --loader ts-node/esm src/index.ts", + "deploy:aws": "node scripts/deploy-aws.js", + "deploy:gcp": "node scripts/deploy-gcp.js", + "deploy:cloudflare": "node scripts/deploy-cloudflare.js" + }, + "keywords": [ + "brainy", + "vector-database", + "cloud", + "aws", + "google-cloud", + "cloudflare" + ], + "author": "David Snelling (david@soulcraft.com)", + "license": "MIT", + "dependencies": { + "@soulcraft/brainy": "^0.9.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "uuid": "^9.0.1", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", + "@types/node": "^20.10.5", + "@types/uuid": "^9.0.7", + "@types/ws": "^8.5.10", + "nodemon": "^3.0.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=23.11.0" + }, + "prettier": { + "arrowParens": "always", + "bracketSameLine": true, + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "css", + "printWidth": 80, + "proseWrap": "preserve", + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false + }, + "eslintConfig": { + "root": true, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "@typescript-eslint/no-explicit-any": "off", + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "after-used", + "argsIgnorePattern": "^_" + } + ], + "semi": [ + "error", + "never" + ], + "@typescript-eslint/semi": [ + "error", + "never" + ] + } + } +} diff --git a/cloud-wrapper/scripts/deploy-aws.js b/cloud-wrapper/scripts/deploy-aws.js new file mode 100644 index 00000000..57a0ec50 --- /dev/null +++ b/cloud-wrapper/scripts/deploy-aws.js @@ -0,0 +1,205 @@ +#!/usr/bin/env node + +/** + * AWS Deployment Script for Brainy Cloud Wrapper + * + * This script helps deploy the Brainy cloud wrapper to AWS Lambda and API Gateway. + * It uses the AWS SDK to create and configure the necessary resources. + * + * Prerequisites: + * - AWS CLI installed and configured with appropriate credentials + * - Node.js 23.11.0 or higher + * - Brainy cloud wrapper built (npm run build) + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Configuration +const config = { + region: process.env.AWS_REGION || 'us-east-1', + functionName: process.env.AWS_FUNCTION_NAME || 'brainy-cloud-service', + s3Bucket: process.env.S3_BUCKET_NAME, + s3Key: process.env.S3_ACCESS_KEY_ID, + s3Secret: process.env.S3_SECRET_ACCESS_KEY, + apiGatewayName: process.env.AWS_API_GATEWAY_NAME || 'brainy-api', + stageName: process.env.AWS_STAGE_NAME || 'prod' +}; + +// Validate configuration +if (!config.s3Bucket && process.env.STORAGE_TYPE === 's3') { + console.error('Error: S3 bucket name is required when using S3 storage'); + process.exit(1); +} + +// Create deployment package +function createDeploymentPackage() { + console.log('Creating deployment package...'); + + try { + // Create a temporary directory for the deployment package + if (!fs.existsSync('deploy')) { + fs.mkdirSync('deploy'); + } + + // Copy necessary files + execSync('cp -r dist deploy/'); + execSync('cp package.json deploy/'); + execSync('cp .env deploy/'); + + // Create a zip file + execSync('cd deploy && zip -r ../deployment.zip .'); + + console.log('Deployment package created successfully'); + } catch (error) { + console.error('Error creating deployment package:', error); + process.exit(1); + } +} + +// Deploy to AWS Lambda +function deployToLambda() { + console.log('Deploying to AWS Lambda...'); + + try { + // Check if function exists + try { + execSync(`aws lambda get-function --function-name ${config.functionName} --region ${config.region}`); + + // Update existing function + execSync(`aws lambda update-function-code --function-name ${config.functionName} --zip-file fileb://deployment.zip --region ${config.region}`); + console.log(`Lambda function ${config.functionName} updated successfully`); + } catch (error) { + // Create new function + execSync(`aws lambda create-function --function-name ${config.functionName} --runtime nodejs20.x --handler dist/index.handler --zip-file fileb://deployment.zip --role arn:aws:iam::${process.env.AWS_ACCOUNT_ID}:role/lambda-basic-execution --region ${config.region}`); + console.log(`Lambda function ${config.functionName} created successfully`); + } + + // Configure environment variables + const envVars = { + Variables: { + NODE_ENV: 'production', + STORAGE_TYPE: process.env.STORAGE_TYPE || 'filesystem', + S3_BUCKET_NAME: config.s3Bucket, + S3_ACCESS_KEY_ID: config.s3Key, + S3_SECRET_ACCESS_KEY: config.s3Secret, + S3_REGION: config.region + } + }; + + execSync(`aws lambda update-function-configuration --function-name ${config.functionName} --environment '${JSON.stringify(envVars)}' --region ${config.region}`); + console.log('Lambda function environment variables configured'); + + } catch (error) { + console.error('Error deploying to AWS Lambda:', error); + process.exit(1); + } +} + +// Create API Gateway +function createApiGateway() { + console.log('Creating API Gateway...'); + + try { + // Check if API Gateway exists + let apiId; + try { + const result = execSync(`aws apigateway get-rest-apis --region ${config.region}`).toString(); + const apis = JSON.parse(result).items; + const api = apis.find(api => api.name === config.apiGatewayName); + + if (api) { + apiId = api.id; + console.log(`Using existing API Gateway: ${apiId}`); + } else { + throw new Error('API Gateway not found'); + } + } catch (error) { + // Create new API Gateway + const result = execSync(`aws apigateway create-rest-api --name ${config.apiGatewayName} --region ${config.region}`).toString(); + apiId = JSON.parse(result).id; + console.log(`API Gateway created: ${apiId}`); + } + + // Get root resource ID + const resourcesResult = execSync(`aws apigateway get-resources --rest-api-id ${apiId} --region ${config.region}`).toString(); + const rootResourceId = JSON.parse(resourcesResult).items.find(resource => resource.path === '/').id; + + // Create proxy resource + let proxyResourceId; + try { + const proxyResource = JSON.parse(resourcesResult).items.find(resource => resource.path === '/{proxy+}'); + if (proxyResource) { + proxyResourceId = proxyResource.id; + } else { + throw new Error('Proxy resource not found'); + } + } catch (error) { + const proxyResult = execSync(`aws apigateway create-resource --rest-api-id ${apiId} --parent-id ${rootResourceId} --path-part {proxy+} --region ${config.region}`).toString(); + proxyResourceId = JSON.parse(proxyResult).id; + } + + // Create ANY method + try { + execSync(`aws apigateway put-method --rest-api-id ${apiId} --resource-id ${proxyResourceId} --http-method ANY --authorization-type NONE --region ${config.region}`); + } catch (error) { + console.log('Method already exists, skipping...'); + } + + // Create integration + try { + execSync(`aws apigateway put-integration --rest-api-id ${apiId} --resource-id ${proxyResourceId} --http-method ANY --type AWS_PROXY --integration-http-method POST --uri arn:aws:apigateway:${config.region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${config.region}:${process.env.AWS_ACCOUNT_ID}:function:${config.functionName}/invocations --region ${config.region}`); + } catch (error) { + console.log('Integration already exists, skipping...'); + } + + // Deploy API + execSync(`aws apigateway create-deployment --rest-api-id ${apiId} --stage-name ${config.stageName} --region ${config.region}`); + + console.log(`API Gateway deployed: https://${apiId}.execute-api.${config.region}.amazonaws.com/${config.stageName}`); + + } catch (error) { + console.error('Error creating API Gateway:', error); + process.exit(1); + } +} + +// Clean up +function cleanup() { + console.log('Cleaning up...'); + + try { + execSync('rm -rf deploy'); + execSync('rm -f deployment.zip'); + + console.log('Cleanup completed'); + } catch (error) { + console.error('Error during cleanup:', error); + } +} + +// Main function +async function main() { + try { + console.log('Starting AWS deployment...'); + + createDeploymentPackage(); + deployToLambda(); + createApiGateway(); + cleanup(); + + console.log('Deployment completed successfully'); + } catch (error) { + console.error('Deployment failed:', error); + cleanup(); + process.exit(1); + } +} + +// Run the script +main(); diff --git a/cloud-wrapper/scripts/deploy-cloudflare.js b/cloud-wrapper/scripts/deploy-cloudflare.js new file mode 100644 index 00000000..be27f626 --- /dev/null +++ b/cloud-wrapper/scripts/deploy-cloudflare.js @@ -0,0 +1,245 @@ +#!/usr/bin/env node + +/** + * Cloudflare Deployment Script for Brainy Cloud Wrapper + * + * This script helps deploy the Brainy cloud wrapper to Cloudflare Workers. + * It uses the Wrangler CLI to create and configure the necessary resources. + * + * Prerequisites: + * - Wrangler CLI installed and configured with appropriate credentials + * - Node.js 23.11.0 or higher + * - Brainy cloud wrapper built (npm run build) + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Configuration +const config = { + workerName: process.env.CF_WORKER_NAME || 'brainy-cloud-service', + accountId: process.env.CF_ACCOUNT_ID, + kvNamespace: process.env.CF_KV_NAMESPACE || 'BRAINY_STORAGE', + r2Bucket: process.env.CF_R2_BUCKET || 'brainy-storage' +}; + +// Validate configuration +if (!config.accountId) { + console.error('Error: CF_ACCOUNT_ID environment variable is required'); + process.exit(1); +} + +// Create wrangler.toml configuration +function createWranglerConfig() { + console.log('Creating wrangler.toml configuration...'); + + const wranglerContent = ` +name = "${config.workerName}" +main = "dist/worker.js" +compatibility_date = "2023-12-01" +node_compat = true + +account_id = "${config.accountId}" + +[build] +command = "npm run build" + +[vars] +NODE_ENV = "production" +STORAGE_TYPE = "${process.env.STORAGE_TYPE || 'memory'}" +USE_SIMPLE_EMBEDDING = "${process.env.USE_SIMPLE_EMBEDDING || 'false'}" + +# KV Namespace for storage +[[kv_namespaces]] +binding = "BRAINY_KV" +id = "${process.env.CF_KV_NAMESPACE_ID || 'create_kv_namespace_and_add_id_here'}" + +# R2 Bucket for storage (if using R2) +[[r2_buckets]] +binding = "BRAINY_R2" +bucket_name = "${config.r2Bucket}" +`; + + try { + fs.writeFileSync('wrangler.toml', wranglerContent); + console.log('wrangler.toml created successfully'); + } catch (error) { + console.error('Error creating wrangler.toml:', error); + process.exit(1); + } +} + +// Create Cloudflare Worker adapter +function createWorkerAdapter() { + console.log('Creating Cloudflare Worker adapter...'); + + const workerContent = ` +import { createServer } from '@cloudflare/workers-adapter'; +import app from './index.js'; + +// Create a fetch handler for the worker +export default { + async fetch(request, env, ctx) { + // Add environment variables to process.env + process.env = { + ...process.env, + ...env.vars, + CF_KV_NAMESPACE: env.BRAINY_KV, + CF_R2_BUCKET: env.BRAINY_R2 + }; + + // Create a server adapter + const server = createServer(app); + + // Handle the request + return server.fetch(request, env, ctx); + } +}; +`; + + try { + // Create dist directory if it doesn't exist + if (!fs.existsSync('dist')) { + fs.mkdirSync('dist'); + } + + fs.writeFileSync('dist/worker.js', workerContent); + console.log('Worker adapter created successfully'); + } catch (error) { + console.error('Error creating worker adapter:', error); + process.exit(1); + } +} + +// Update package.json to include Cloudflare Workers dependencies +function updatePackageJson() { + console.log('Updating package.json...'); + + try { + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); + + // Add Cloudflare Workers dependencies + packageJson.dependencies = { + ...packageJson.dependencies, + '@cloudflare/workers-adapter': '^1.1.0', + '@cloudflare/kv-asset-handler': '^0.3.0' + }; + + // Add Cloudflare Workers scripts + packageJson.scripts = { + ...packageJson.scripts, + 'deploy:cloudflare': 'wrangler deploy' + }; + + fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2)); + console.log('package.json updated successfully'); + + // Install new dependencies + execSync('npm install --legacy-peer-deps'); + } catch (error) { + console.error('Error updating package.json:', error); + process.exit(1); + } +} + +// Create KV namespace if it doesn't exist +function createKVNamespace() { + console.log('Creating KV namespace...'); + + try { + // Check if KV namespace ID is provided + if (process.env.CF_KV_NAMESPACE_ID) { + console.log(`Using existing KV namespace: ${process.env.CF_KV_NAMESPACE_ID}`); + return; + } + + // Create KV namespace + const result = execSync(`wrangler kv:namespace create "${config.kvNamespace}"`).toString(); + const match = result.match(/id = "([^"]+)"/); + + if (match && match[1]) { + const namespaceId = match[1]; + console.log(`KV namespace created with ID: ${namespaceId}`); + + // Update wrangler.toml with the new namespace ID + let wranglerContent = fs.readFileSync('wrangler.toml', 'utf8'); + wranglerContent = wranglerContent.replace(/id = "create_kv_namespace_and_add_id_here"/, `id = "${namespaceId}"`); + fs.writeFileSync('wrangler.toml', wranglerContent); + } else { + console.error('Failed to extract KV namespace ID from wrangler output'); + } + } catch (error) { + console.error('Error creating KV namespace:', error); + console.log('You may need to create the KV namespace manually and update wrangler.toml'); + } +} + +// Create R2 bucket if it doesn't exist +function createR2Bucket() { + console.log('Creating R2 bucket...'); + + try { + // Only create R2 bucket if using R2 storage + if (process.env.STORAGE_TYPE !== 'r2') { + console.log('Skipping R2 bucket creation (not using R2 storage)'); + return; + } + + // Create R2 bucket + execSync(`wrangler r2 bucket create ${config.r2Bucket}`); + console.log(`R2 bucket created: ${config.r2Bucket}`); + } catch (error) { + console.error('Error creating R2 bucket:', error); + console.log('You may need to create the R2 bucket manually'); + } +} + +// Deploy to Cloudflare Workers +function deployToCloudflare() { + console.log('Deploying to Cloudflare Workers...'); + + try { + execSync('wrangler deploy', { stdio: 'inherit' }); + console.log('Deployed to Cloudflare Workers successfully'); + } catch (error) { + console.error('Error deploying to Cloudflare Workers:', error); + process.exit(1); + } +} + +// Clean up +function cleanup() { + console.log('Cleaning up...'); + + // No cleanup needed for now + console.log('Cleanup completed'); +} + +// Main function +async function main() { + try { + console.log('Starting Cloudflare deployment...'); + + createWranglerConfig(); + createWorkerAdapter(); + updatePackageJson(); + createKVNamespace(); + createR2Bucket(); + deployToCloudflare(); + cleanup(); + + console.log('Deployment completed successfully'); + } catch (error) { + console.error('Deployment failed:', error); + cleanup(); + process.exit(1); + } +} + +// Run the script +main(); diff --git a/cloud-wrapper/scripts/deploy-gcp.js b/cloud-wrapper/scripts/deploy-gcp.js new file mode 100644 index 00000000..8f388720 --- /dev/null +++ b/cloud-wrapper/scripts/deploy-gcp.js @@ -0,0 +1,196 @@ +#!/usr/bin/env node + +/** + * Google Cloud Platform Deployment Script for Brainy Cloud Wrapper + * + * This script helps deploy the Brainy cloud wrapper to Google Cloud Run. + * It uses the Google Cloud SDK to create and configure the necessary resources. + * + * Prerequisites: + * - Google Cloud SDK installed and configured with appropriate credentials + * - Node.js 23.11.0 or higher + * - Brainy cloud wrapper built (npm run build) + * - Docker installed (for building container images) + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Configuration +const config = { + projectId: process.env.GCP_PROJECT_ID, + region: process.env.GCP_REGION || 'us-central1', + serviceName: process.env.GCP_SERVICE_NAME || 'brainy-cloud-service', + imageName: process.env.GCP_IMAGE_NAME || 'brainy-cloud-service', + memory: process.env.GCP_MEMORY || '512Mi', + cpu: process.env.GCP_CPU || '1', + maxInstances: process.env.GCP_MAX_INSTANCES || '10', + minInstances: process.env.GCP_MIN_INSTANCES || '0' +}; + +// Validate configuration +if (!config.projectId) { + console.error('Error: GCP_PROJECT_ID environment variable is required'); + process.exit(1); +} + +// Create Dockerfile +function createDockerfile() { + console.log('Creating Dockerfile...'); + + const dockerfileContent = ` +FROM node:23-slim + +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install --production --legacy-peer-deps + +# Copy built application +COPY dist/ ./dist/ + +# Copy environment variables +COPY .env ./ + +# Expose port +EXPOSE 8080 + +# Start the application +CMD ["node", "dist/index.js"] +`; + + try { + fs.writeFileSync('Dockerfile', dockerfileContent); + console.log('Dockerfile created successfully'); + } catch (error) { + console.error('Error creating Dockerfile:', error); + process.exit(1); + } +} + +// Build and push Docker image +function buildAndPushImage() { + console.log('Building and pushing Docker image...'); + + try { + // Set Google Cloud project + execSync(`gcloud config set project ${config.projectId}`); + + // Build the Docker image + execSync(`docker build -t gcr.io/${config.projectId}/${config.imageName} .`); + + // Configure Docker to use gcloud as a credential helper + execSync('gcloud auth configure-docker'); + + // Push the image to Google Container Registry + execSync(`docker push gcr.io/${config.projectId}/${config.imageName}`); + + console.log('Docker image built and pushed successfully'); + } catch (error) { + console.error('Error building and pushing Docker image:', error); + process.exit(1); + } +} + +// Deploy to Google Cloud Run +function deployToCloudRun() { + console.log('Deploying to Google Cloud Run...'); + + try { + // Create environment variables string + const envVars = [ + `NODE_ENV=production`, + `PORT=8080`, + `STORAGE_TYPE=${process.env.STORAGE_TYPE || 'filesystem'}` + ]; + + // Add S3 environment variables if using S3 storage + if (process.env.STORAGE_TYPE === 's3') { + envVars.push(`S3_BUCKET_NAME=${process.env.S3_BUCKET_NAME}`); + envVars.push(`S3_ACCESS_KEY_ID=${process.env.S3_ACCESS_KEY_ID}`); + envVars.push(`S3_SECRET_ACCESS_KEY=${process.env.S3_SECRET_ACCESS_KEY}`); + envVars.push(`S3_REGION=${process.env.S3_REGION || 'us-east-1'}`); + + if (process.env.S3_ENDPOINT) { + envVars.push(`S3_ENDPOINT=${process.env.S3_ENDPOINT}`); + } + } + + // Add embedding and HNSW configuration if provided + if (process.env.USE_SIMPLE_EMBEDDING) { + envVars.push(`USE_SIMPLE_EMBEDDING=${process.env.USE_SIMPLE_EMBEDDING}`); + } + + if (process.env.HNSW_M) { + envVars.push(`HNSW_M=${process.env.HNSW_M}`); + envVars.push(`HNSW_EF_CONSTRUCTION=${process.env.HNSW_EF_CONSTRUCTION || '200'}`); + envVars.push(`HNSW_EF_SEARCH=${process.env.HNSW_EF_SEARCH || '50'}`); + } + + // Deploy to Cloud Run + const envVarsString = envVars.map(env => `--set-env-vars="${env}"`).join(' '); + + execSync(`gcloud run deploy ${config.serviceName} \ + --image=gcr.io/${config.projectId}/${config.imageName} \ + --platform=managed \ + --region=${config.region} \ + --memory=${config.memory} \ + --cpu=${config.cpu} \ + --max-instances=${config.maxInstances} \ + --min-instances=${config.minInstances} \ + --allow-unauthenticated \ + ${envVarsString}`); + + console.log('Deployed to Google Cloud Run successfully'); + + // Get the service URL + const serviceUrl = execSync(`gcloud run services describe ${config.serviceName} --region=${config.region} --format="value(status.url)"`).toString().trim(); + console.log(`Service URL: ${serviceUrl}`); + } catch (error) { + console.error('Error deploying to Google Cloud Run:', error); + process.exit(1); + } +} + +// Clean up +function cleanup() { + console.log('Cleaning up...'); + + try { + // Remove Dockerfile + fs.unlinkSync('Dockerfile'); + + console.log('Cleanup completed'); + } catch (error) { + console.error('Error during cleanup:', error); + } +} + +// Main function +async function main() { + try { + console.log('Starting Google Cloud Platform deployment...'); + + createDockerfile(); + buildAndPushImage(); + deployToCloudRun(); + cleanup(); + + console.log('Deployment completed successfully'); + } catch (error) { + console.error('Deployment failed:', error); + cleanup(); + process.exit(1); + } +} + +// Run the script +main(); diff --git a/cloud-wrapper/src/index.ts b/cloud-wrapper/src/index.ts new file mode 100644 index 00000000..f820daff --- /dev/null +++ b/cloud-wrapper/src/index.ts @@ -0,0 +1,106 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import dotenv from 'dotenv'; +import http from 'http'; +import { WebSocketServer } from 'ws'; +import { BrainyData } from '@soulcraft/brainy'; +import { setupRoutes } from './routes.js'; +import { initializeBrainy } from './services/brainyService.js'; +import { setupWebSocketHandlers } from './websocket.js'; +import { initializeMCPService } from './services/mcpService.js'; + +// Load environment variables +dotenv.config(); + +// Initialize Express app +const app = express(); +const port = process.env.PORT || 3000; + +// Middleware +app.use(helmet()); // Security headers +app.use(cors()); // Enable CORS +app.use(morgan('combined')); // Logging +app.use(express.json()); // Parse JSON bodies + +// Initialize Brainy +let brainyInstance: BrainyData; + +// Health check endpoint +app.get('/health', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); + +// Setup API routes +const apiRouter = setupRoutes(); +app.use('/api', (req, res, next) => { + // Attach brainy instance to request + (req as any).brainy = brainyInstance; + next(); +}, apiRouter); + +// Start the server +async function startServer() { + try { + // Initialize Brainy + brainyInstance = await initializeBrainy(); + console.log('Brainy initialized successfully'); + + // Create HTTP server + const server = http.createServer(app); + + // Create WebSocket server + const wss = new WebSocketServer({ server }); + + // Setup WebSocket handlers + setupWebSocketHandlers(wss, brainyInstance); + console.log('WebSocket server initialized'); + + // Initialize MCP service + const mcpWsPort = process.env.MCP_WS_PORT ? parseInt(process.env.MCP_WS_PORT, 10) : undefined; + const mcpRestPort = process.env.MCP_REST_PORT ? parseInt(process.env.MCP_REST_PORT, 10) : undefined; + + if (mcpWsPort || mcpRestPort) { + initializeMCPService(brainyInstance, { + wsPort: mcpWsPort, + restPort: mcpRestPort, + enableAuth: process.env.MCP_ENABLE_AUTH === 'true', + apiKeys: process.env.MCP_API_KEYS ? process.env.MCP_API_KEYS.split(',') : undefined, + rateLimit: process.env.MCP_RATE_LIMIT_REQUESTS ? { + windowMs: parseInt(process.env.MCP_RATE_LIMIT_WINDOW_MS || '60000', 10), + maxRequests: parseInt(process.env.MCP_RATE_LIMIT_REQUESTS, 10) + } : undefined, + cors: process.env.MCP_ENABLE_CORS === 'true' ? {} : undefined + }); + console.log('MCP service initialized'); + } + + // Start HTTP server + server.listen(port, () => { + console.log(`Server running on port ${port}`); + console.log(`WebSocket server running on ws://localhost:${port}`); + }); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +} + +// Handle graceful shutdown +process.on('SIGTERM', async () => { + console.log('SIGTERM received, shutting down gracefully'); + // Cleanup Brainy resources + await brainyInstance?.clear(); + process.exit(0); +}); + +process.on('SIGINT', async () => { + console.log('SIGINT received, shutting down gracefully'); + // Cleanup Brainy resources + await brainyInstance?.clear(); + process.exit(0); +}); + +// Start the server +startServer(); diff --git a/cloud-wrapper/src/routes.ts b/cloud-wrapper/src/routes.ts new file mode 100644 index 00000000..735a498d --- /dev/null +++ b/cloud-wrapper/src/routes.ts @@ -0,0 +1,186 @@ +import { Router, Request, Response, NextFunction, RequestHandler } from 'express'; +import { BrainyData, NounType, VerbType } from '@soulcraft/brainy'; + +// Define a custom request type that includes the Brainy instance +interface BrainyRequest extends Request { + brainy: BrainyData; +} + +// Helper function to handle async route handlers +const asyncHandler = (fn: (req: BrainyRequest, res: Response, next: NextFunction) => Promise) => + (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req as BrainyRequest, res, next)).catch(next); + }; + +export function setupRoutes() { + const router = Router(); + + // Get database status + router.get('/status', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const status = await req.brainy.status(); + res.status(200).json(status); + } catch (error) { + console.error('Error getting status:', error); + res.status(500).json({ error: 'Failed to get database status' }); + } + })); + + // Add a noun (entity) + router.post('/nouns', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { text, metadata } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text is required' }); + } + + const id = await req.brainy.add(text, metadata || {}); + res.status(201).json({ id }); + } catch (error) { + console.error('Error adding noun:', error); + res.status(500).json({ error: 'Failed to add noun' }); + } + })); + + // Get a noun by ID + router.get('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const noun = await req.brainy.get(id); + + if (!noun) { + return res.status(404).json({ error: 'Noun not found' }); + } + + res.status(200).json(noun); + } catch (error) { + console.error('Error getting noun:', error); + res.status(500).json({ error: 'Failed to get noun' }); + } + })); + + // Update noun metadata + router.put('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const { metadata } = req.body; + + if (!metadata) { + return res.status(400).json({ error: 'Metadata is required' }); + } + + await req.brainy.updateMetadata(id, metadata); + res.status(200).json({ success: true }); + } catch (error) { + console.error('Error updating noun:', error); + res.status(500).json({ error: 'Failed to update noun' }); + } + })); + + // Delete a noun + router.delete('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + await req.brainy.delete(id); + res.status(200).json({ success: true }); + } catch (error) { + console.error('Error deleting noun:', error); + res.status(500).json({ error: 'Failed to delete noun' }); + } + })); + + // Search for similar nouns + router.post('/search', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { query, limit = 10 } = req.body; + + if (!query) { + return res.status(400).json({ error: 'Query is required' }); + } + + const results = await req.brainy.searchText(query, limit); + res.status(200).json(results); + } catch (error) { + console.error('Error searching:', error); + res.status(500).json({ error: 'Failed to search' }); + } + })); + + // Add a verb (relationship) + router.post('/verbs', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { sourceId, targetId, metadata } = req.body; + + if (!sourceId || !targetId) { + return res.status(400).json({ error: 'Source ID and Target ID are required' }); + } + + await req.brainy.addVerb(sourceId, targetId, metadata || {}); + res.status(201).json({ success: true }); + } catch (error) { + console.error('Error adding verb:', error); + res.status(500).json({ error: 'Failed to add verb' }); + } + })); + + // Get all verbs + router.get('/verbs', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const verbs = await req.brainy.getAllVerbs(); + res.status(200).json(verbs); + } catch (error) { + console.error('Error getting verbs:', error); + res.status(500).json({ error: 'Failed to get verbs' }); + } + })); + + // Get verbs by source + router.get('/verbs/source/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const verbs = await req.brainy.getVerbsBySource(id); + res.status(200).json(verbs); + } catch (error) { + console.error('Error getting verbs by source:', error); + res.status(500).json({ error: 'Failed to get verbs by source' }); + } + })); + + // Get verbs by target + router.get('/verbs/target/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const verbs = await req.brainy.getVerbsByTarget(id); + res.status(200).json(verbs); + } catch (error) { + console.error('Error getting verbs by target:', error); + res.status(500).json({ error: 'Failed to get verbs by target' }); + } + })); + + // Delete a verb + router.delete('/verbs/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + await req.brainy.deleteVerb(id); + res.status(200).json({ success: true }); + } catch (error) { + console.error('Error deleting verb:', error); + res.status(500).json({ error: 'Failed to delete verb' }); + } + })); + + // Clear all data + router.delete('/clear', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => { + try { + await req.brainy.clear(); + res.status(200).json({ success: true }); + } catch (error) { + console.error('Error clearing data:', error); + res.status(500).json({ error: 'Failed to clear data' }); + } + })); + + return router; +} diff --git a/cloud-wrapper/src/services/brainyService.ts b/cloud-wrapper/src/services/brainyService.ts new file mode 100644 index 00000000..8fa21853 --- /dev/null +++ b/cloud-wrapper/src/services/brainyService.ts @@ -0,0 +1,97 @@ +import { BrainyData, BrainyDataConfig } from '@soulcraft/brainy' +import dotenv from 'dotenv' + +// Load environment variables +dotenv.config() + +/** + * Extended configuration interface that includes rootDirectory + */ +interface ExtendedBrainyDataConfig extends BrainyDataConfig { + storage: BrainyDataConfig['storage'] & { + rootDirectory?: string + } +} + +/** + * Initialize the Brainy instance with appropriate configuration + * based on environment variables + */ +export async function initializeBrainy(): Promise { + // Get storage configuration from environment variables + const storageType = process.env.STORAGE_TYPE || 'filesystem' + + // Create configuration object + const config: ExtendedBrainyDataConfig = { + storage: {} + } + + // Configure storage based on type + switch (storageType) { + case 's3': + // Configure S3 storage + if (process.env.S3_ENDPOINT) { + // Use customS3Storage for S3-compatible services with custom endpoints + config.storage.customS3Storage = { + bucketName: process.env.S3_BUCKET_NAME, + accessKeyId: process.env.S3_ACCESS_KEY_ID, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, + region: process.env.S3_REGION || 'us-east-1', + endpoint: process.env.S3_ENDPOINT + } + } else { + // Use standard S3 storage for AWS S3 + config.storage.s3Storage = { + bucketName: process.env.S3_BUCKET_NAME, + accessKeyId: process.env.S3_ACCESS_KEY_ID, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, + region: process.env.S3_REGION || 'us-east-1' + } + } + break + + case 'filesystem': + // Configure filesystem storage + config.storage.rootDirectory = process.env.STORAGE_ROOT_DIR || './data' + break + + case 'memory': + // No additional configuration needed for memory storage + break + + default: + // Default to filesystem storage + config.storage.rootDirectory = process.env.STORAGE_ROOT_DIR || './data' + break + } + + // Note: Universal Sentence Encoder is now the only embedding option + // TensorFlow.js is required for embedding to work + + // Configure HNSW index parameters if provided + if (process.env.HNSW_M) { + config.hnsw = { + M: parseInt(process.env.HNSW_M, 10), + efConstruction: process.env.HNSW_EF_CONSTRUCTION + ? parseInt(process.env.HNSW_EF_CONSTRUCTION, 10) + : 200, + efSearch: process.env.HNSW_EF_SEARCH + ? parseInt(process.env.HNSW_EF_SEARCH, 10) + : 50 + } + } + + // Create and initialize the Brainy instance + const brainy = new BrainyData(config) + await brainy.init() + + return brainy +} + +/** + * Get the current storage type being used by Brainy + */ +export async function getStorageType(brainy: BrainyData): Promise { + const status = await brainy.status() + return status.type || 'unknown' +} diff --git a/cloud-wrapper/src/services/mcpService.ts b/cloud-wrapper/src/services/mcpService.ts new file mode 100644 index 00000000..15809564 --- /dev/null +++ b/cloud-wrapper/src/services/mcpService.ts @@ -0,0 +1,204 @@ +import { WebSocketServer } from 'ws' +import express from 'express' +import cors from 'cors' +import { BrainyData, BrainyMCPService, MCPRequestType } from '@soulcraft/brainy' +import { v4 as uuidv4 } from 'uuid' + +/** + * Initialize the MCP service with WebSocket and REST API servers + */ +export function initializeMCPService( + brainy: BrainyData, + options: { + wsPort?: number + restPort?: number + enableAuth?: boolean + apiKeys?: string[] + rateLimit?: { + windowMs: number + maxRequests: number + } + cors?: any + } +) { + // Create the MCP service + const mcpService = new BrainyMCPService(brainy, options) + + // Start WebSocket server if port is provided + if (options.wsPort) { + startWebSocketServer(mcpService, options.wsPort) + } + + // Start REST server if port is provided + if (options.restPort) { + startRESTServer(mcpService, options.restPort, options.cors) + } + + return mcpService +} + +/** + * Start a WebSocket server for the MCP service + */ +function startWebSocketServer(mcpService: BrainyMCPService, port: number) { + const wss = new WebSocketServer({ port }) + + wss.on('connection', (ws: any) => { + ws.on('message', async (message: string) => { + try { + const request = JSON.parse(message) + + // Handle the request using the MCP service + const response = await mcpService.handleMCPRequest(request) + + // Send the response + ws.send(JSON.stringify(response)) + } catch (error) { + // Send error response + ws.send( + JSON.stringify({ + success: false, + requestId: uuidv4(), + error: { + code: 'INTERNAL_ERROR', + message: error instanceof Error ? error.message : String(error) + } + }) + ) + } + }) + }) + + console.log(`MCP WebSocket server started on port ${port}`) + + return wss +} + +/** + * Start a REST server for the MCP service + */ +function startRESTServer( + mcpService: BrainyMCPService, + port: number, + corsOptions?: any +) { + const app = express() + + // Parse JSON request bodies + app.use(express.json()) + + // Enable CORS if configured + if (corsOptions) { + app.use(cors(corsOptions)) + } + + // MCP endpoints + app.post('/mcp/data', async (req: any, res: any) => { + try { + const response = await mcpService.handleMCPRequest({ + ...req.body, + type: 'DATA_ACCESS' + }) + + res.json(response) + } catch (error) { + res.status(500).json({ + success: false, + requestId: uuidv4(), + error: { + code: 'INTERNAL_ERROR', + message: error instanceof Error ? error.message : String(error) + } + }) + } + }) + + app.post('/mcp/tools', async (req: any, res: any) => { + try { + const response = await mcpService.handleMCPRequest({ + ...req.body, + type: 'TOOL_EXECUTION' + }) + + res.json(response) + } catch (error) { + res.status(500).json({ + success: false, + requestId: uuidv4(), + error: { + code: 'INTERNAL_ERROR', + message: error instanceof Error ? error.message : String(error) + } + }) + } + }) + + app.post('/mcp/system', async (req: any, res: any) => { + try { + const response = await mcpService.handleMCPRequest({ + ...req.body, + type: 'SYSTEM_INFO' + }) + + res.json(response) + } catch (error) { + res.status(500).json({ + success: false, + requestId: uuidv4(), + error: { + code: 'INTERNAL_ERROR', + message: error instanceof Error ? error.message : String(error) + } + }) + } + }) + + app.post('/mcp/auth', async (req: any, res: any) => { + try { + const response = await mcpService.handleMCPRequest({ + ...req.body, + type: 'AUTHENTICATION' + }) + + res.json(response) + } catch (error) { + res.status(500).json({ + success: false, + requestId: uuidv4(), + error: { + code: 'INTERNAL_ERROR', + message: error instanceof Error ? error.message : String(error) + } + }) + } + }) + + // Get available tools + app.get('/mcp/tools', async (req: any, res: any) => { + try { + const response = await mcpService.handleMCPRequest({ + type: MCPRequestType.SYSTEM_INFO, + requestId: uuidv4(), + version: '1.0' + }) + + res.json(response) + } catch (error) { + res.status(500).json({ + success: false, + requestId: uuidv4(), + error: { + code: 'INTERNAL_ERROR', + message: error instanceof Error ? error.message : String(error) + } + }) + } + }) + + // Start the server + const server = app.listen(port, () => { + console.log(`MCP REST API server started on port ${port}`) + }) + + return server +} diff --git a/cloud-wrapper/src/websocket.ts b/cloud-wrapper/src/websocket.ts new file mode 100644 index 00000000..5e6b5714 --- /dev/null +++ b/cloud-wrapper/src/websocket.ts @@ -0,0 +1,433 @@ +import { WebSocketServer, WebSocket } from 'ws'; +import { BrainyData } from '@soulcraft/brainy'; +import { v4 as uuidv4 } from 'uuid'; + +// Define message types +enum MessageType { + STATUS = 'status', + ADD_NOUN = 'addNoun', + GET_NOUN = 'getNoun', + UPDATE_NOUN = 'updateNoun', + DELETE_NOUN = 'deleteNoun', + SEARCH = 'search', + ADD_VERB = 'addVerb', + GET_VERBS = 'getVerbs', + GET_VERBS_BY_SOURCE = 'getVerbsBySource', + GET_VERBS_BY_TARGET = 'getVerbsByTarget', + DELETE_VERB = 'deleteVerb', + CLEAR = 'clear', + SUBSCRIBE = 'subscribe', + UNSUBSCRIBE = 'unsubscribe', + ERROR = 'error' +} + +// Define subscription types +enum SubscriptionType { + NOUNS = 'nouns', + VERBS = 'verbs', + SEARCH_RESULTS = 'searchResults' +} + +// Define message interface +interface WebSocketMessage { + type: MessageType; + id?: string; + payload?: any; +} + +// Define client interface +interface WebSocketClient { + id: string; + socket: WebSocket; + subscriptions: Set; +} + +// Store connected clients +const clients: Map = new Map(); + +// Setup WebSocket handlers +export function setupWebSocketHandlers(wss: WebSocketServer, brainy: BrainyData) { + // Handle new connections + wss.on('connection', (socket: WebSocket) => { + const clientId = uuidv4(); + + // Create client object + const client: WebSocketClient = { + id: clientId, + socket, + subscriptions: new Set() + }; + + // Store client + clients.set(clientId, client); + + console.log(`WebSocket client connected: ${clientId}`); + + // Send welcome message + sendMessage(socket, { + type: MessageType.STATUS, + id: uuidv4(), + payload: { + clientId, + message: 'Connected to Brainy WebSocket API', + status: 'connected' + } + }); + + // Handle messages + socket.on('message', async (data: WebSocket.Data) => { + try { + const message: WebSocketMessage = JSON.parse(data.toString()); + + // Ensure message has an ID + const messageId = message.id || uuidv4(); + + console.log(`Received message: ${message.type} (${messageId})`); + + // Process message based on type + await processMessage(client, message, messageId, brainy); + } catch (error) { + console.error('Error processing WebSocket message:', error); + sendMessage(socket, { + type: MessageType.ERROR, + id: uuidv4(), + payload: { + message: 'Invalid message format', + error: (error as Error).message + } + }); + } + }); + + // Handle disconnection + socket.on('close', () => { + // Remove client + clients.delete(clientId); + console.log(`WebSocket client disconnected: ${clientId}`); + }); + }); +} + +// Process incoming messages +async function processMessage( + client: WebSocketClient, + message: WebSocketMessage, + messageId: string, + brainy: BrainyData +) { + const { socket } = client; + + try { + switch (message.type) { + case MessageType.STATUS: + // Return database status + const status = await brainy.status(); + sendMessage(socket, { + type: MessageType.STATUS, + id: messageId, + payload: status + }); + break; + + case MessageType.ADD_NOUN: + // Add a noun + if (!message.payload?.text) { + throw new Error('Text is required'); + } + + const nounId = await brainy.add( + message.payload.text, + message.payload.metadata || {} + ); + + sendMessage(socket, { + type: MessageType.ADD_NOUN, + id: messageId, + payload: { id: nounId } + }); + + // Notify subscribers + notifySubscribers(SubscriptionType.NOUNS, { + type: 'added', + id: nounId, + data: await brainy.get(nounId) + }); + + break; + + case MessageType.GET_NOUN: + // Get a noun by ID + if (!message.payload?.id) { + throw new Error('Noun ID is required'); + } + + const noun = await brainy.get(message.payload.id); + + if (!noun) { + throw new Error('Noun not found'); + } + + sendMessage(socket, { + type: MessageType.GET_NOUN, + id: messageId, + payload: noun + }); + break; + + case MessageType.UPDATE_NOUN: + // Update noun metadata + if (!message.payload?.id || !message.payload?.metadata) { + throw new Error('Noun ID and metadata are required'); + } + + await brainy.updateMetadata(message.payload.id, message.payload.metadata); + + sendMessage(socket, { + type: MessageType.UPDATE_NOUN, + id: messageId, + payload: { success: true } + }); + + // Notify subscribers + notifySubscribers(SubscriptionType.NOUNS, { + type: 'updated', + id: message.payload.id, + data: await brainy.get(message.payload.id) + }); + + break; + + case MessageType.DELETE_NOUN: + // Delete a noun + if (!message.payload?.id) { + throw new Error('Noun ID is required'); + } + + await brainy.delete(message.payload.id); + + sendMessage(socket, { + type: MessageType.DELETE_NOUN, + id: messageId, + payload: { success: true } + }); + + // Notify subscribers + notifySubscribers(SubscriptionType.NOUNS, { + type: 'deleted', + id: message.payload.id + }); + + break; + + case MessageType.SEARCH: + // Search for similar nouns + if (!message.payload?.query) { + throw new Error('Query is required'); + } + + const limit = message.payload.limit || 10; + const results = await brainy.searchText(message.payload.query, limit); + + sendMessage(socket, { + type: MessageType.SEARCH, + id: messageId, + payload: results + }); + + // Notify subscribers + notifySubscribers(SubscriptionType.SEARCH_RESULTS, { + type: 'search', + query: message.payload.query, + results + }); + + break; + + case MessageType.ADD_VERB: + // Add a verb (relationship) + if (!message.payload?.sourceId || !message.payload?.targetId) { + throw new Error('Source ID and Target ID are required'); + } + + await brainy.addVerb( + message.payload.sourceId, + message.payload.targetId, + message.payload.metadata || {} + ); + + sendMessage(socket, { + type: MessageType.ADD_VERB, + id: messageId, + payload: { success: true } + }); + + // Notify subscribers + notifySubscribers(SubscriptionType.VERBS, { + type: 'added', + sourceId: message.payload.sourceId, + targetId: message.payload.targetId, + metadata: message.payload.metadata + }); + + break; + + case MessageType.GET_VERBS: + // Get all verbs + const verbs = await brainy.getAllVerbs(); + + sendMessage(socket, { + type: MessageType.GET_VERBS, + id: messageId, + payload: verbs + }); + break; + + case MessageType.GET_VERBS_BY_SOURCE: + // Get verbs by source + if (!message.payload?.id) { + throw new Error('Source ID is required'); + } + + const sourceVerbs = await brainy.getVerbsBySource(message.payload.id); + + sendMessage(socket, { + type: MessageType.GET_VERBS_BY_SOURCE, + id: messageId, + payload: sourceVerbs + }); + break; + + case MessageType.GET_VERBS_BY_TARGET: + // Get verbs by target + if (!message.payload?.id) { + throw new Error('Target ID is required'); + } + + const targetVerbs = await brainy.getVerbsByTarget(message.payload.id); + + sendMessage(socket, { + type: MessageType.GET_VERBS_BY_TARGET, + id: messageId, + payload: targetVerbs + }); + break; + + case MessageType.DELETE_VERB: + // Delete a verb + if (!message.payload?.id) { + throw new Error('Verb ID is required'); + } + + await brainy.deleteVerb(message.payload.id); + + sendMessage(socket, { + type: MessageType.DELETE_VERB, + id: messageId, + payload: { success: true } + }); + + // Notify subscribers + notifySubscribers(SubscriptionType.VERBS, { + type: 'deleted', + id: message.payload.id + }); + + break; + + case MessageType.CLEAR: + // Clear all data + await brainy.clear(); + + sendMessage(socket, { + type: MessageType.CLEAR, + id: messageId, + payload: { success: true } + }); + + // Notify all subscribers + notifySubscribers(SubscriptionType.NOUNS, { type: 'cleared' }); + notifySubscribers(SubscriptionType.VERBS, { type: 'cleared' }); + + break; + + case MessageType.SUBSCRIBE: + // Subscribe to events + if (!message.payload?.type) { + throw new Error('Subscription type is required'); + } + + const subscriptionType = message.payload.type as SubscriptionType; + + // Add subscription + client.subscriptions.add(subscriptionType); + + sendMessage(socket, { + type: MessageType.SUBSCRIBE, + id: messageId, + payload: { + success: true, + type: subscriptionType + } + }); + break; + + case MessageType.UNSUBSCRIBE: + // Unsubscribe from events + if (!message.payload?.type) { + throw new Error('Subscription type is required'); + } + + const unsubscribeType = message.payload.type as SubscriptionType; + + // Remove subscription + client.subscriptions.delete(unsubscribeType); + + sendMessage(socket, { + type: MessageType.UNSUBSCRIBE, + id: messageId, + payload: { + success: true, + type: unsubscribeType + } + }); + break; + + default: + throw new Error(`Unknown message type: ${message.type}`); + } + } catch (error) { + console.error(`Error processing message ${message.type}:`, error); + + sendMessage(socket, { + type: MessageType.ERROR, + id: messageId, + payload: { + originalType: message.type, + message: 'Error processing message', + error: (error as Error).message + } + }); + } +} + +// Send a message to a client +function sendMessage(socket: WebSocket, message: WebSocketMessage) { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(message)); + } +} + +// Notify subscribers of events +function notifySubscribers(type: SubscriptionType, data: any) { + for (const client of clients.values()) { + if (client.subscriptions.has(type)) { + sendMessage(client.socket, { + type: MessageType.SUBSCRIBE, + payload: { + type, + data + } + }); + } + } +} diff --git a/cloud-wrapper/tsconfig.json b/cloud-wrapper/tsconfig.json new file mode 100644 index 00000000..371697c0 --- /dev/null +++ b/cloud-wrapper/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "declaration": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 00000000..7f8293cd --- /dev/null +++ b/demo/index.html @@ -0,0 +1,4255 @@ + + + + + + Brainy Interactive Demo - A Lightweight Graph & Vector Data Platform + + + + + + + + + + + + + + diff --git a/encoded-image.html b/encoded-image.html new file mode 100644 index 00000000..cfc1a5e2 --- /dev/null +++ b/encoded-image.html @@ -0,0 +1 @@ +Brainy Logo \ No newline at end of file diff --git a/encoded-image.txt b/encoded-image.txt new file mode 100644 index 00000000..00a5b8c4 --- /dev/null +++ b/encoded-image.txt @@ -0,0 +1 @@ +Brainy Logo diff --git a/index.html b/index.html new file mode 100644 index 00000000..1018e443 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + Brainy Interactive Demo - Redirecting... + + + + + +

Redirecting to Brainy Interactive Demo...

+

If you are not redirected automatically, please click the link above.

+ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..6cd23e42 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8315 @@ +{ + "name": "@soulcraft/brainy", + "version": "0.9.10", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@soulcraft/brainy", + "version": "0.9.10", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-s3": "^3.427.0", + "@tensorflow-models/universal-sentence-encoder": "^1.3.3", + "@tensorflow/tfjs": "^4.22.0", + "@tensorflow/tfjs-backend-cpu": "^4.22.0", + "@tensorflow/tfjs-converter": "^4.22.0", + "@tensorflow/tfjs-core": "^4.22.0", + "@tensorflow/tfjs-layers": "^4.22.0", + "buffer": "^6.0.3", + "commander": "^14.0.0", + "omelette": "^0.4.17", + "uuid": "^9.0.0" + }, + "bin": { + "brainy": "cli-wrapper.js" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-typescript": "^11.1.6", + "@types/jest": "^29.5.3", + "@types/node": "^20.4.5", + "@types/omelette": "^0.4.5", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.45.0", + "gh-pages": "^6.3.0", + "jest": "^29.6.2", + "rollup": "^4.12.0", + "rollup-plugin-terser": "^7.0.2", + "ts-jest": "^29.1.1", + "tslib": "^2.8.1", + "typescript": "^5.1.6" + }, + "engines": { + "node": ">=23.11.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.824.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.824.0.tgz", + "integrity": "sha512-7neTQIdSVP/F4RTWG5T87LDpB955iQD6lxg9nJ00fdkIPczDcRtAEXow44NjF4fEdpQ1A9jokUtBSVE+GMXZ/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.823.0", + "@aws-sdk/credential-provider-node": "3.823.0", + "@aws-sdk/middleware-bucket-endpoint": "3.821.0", + "@aws-sdk/middleware-expect-continue": "3.821.0", + "@aws-sdk/middleware-flexible-checksums": "3.823.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-location-constraint": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-sdk-s3": "3.823.0", + "@aws-sdk/middleware-ssec": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.823.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/signature-v4-multi-region": "3.824.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.821.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.823.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.1", + "@smithy/eventstream-serde-browser": "^4.0.4", + "@smithy/eventstream-serde-config-resolver": "^4.1.2", + "@smithy/eventstream-serde-node": "^4.0.4", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-blob-browser": "^4.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/hash-stream-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/md5-js": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.9", + "@smithy/middleware-retry": "^4.1.10", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.1", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.17", + "@smithy/util-defaults-mode-node": "^4.0.17", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.823.0.tgz", + "integrity": "sha512-dBWdsbyGw8rPfdCsZySNtTOGQK4EZ8lxB/CneSQWRBPHgQ+Ys88NXxImO8xfWO7Itt1eh8O7UDTZ9+smcvw2pw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.823.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.823.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.821.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.823.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.1", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.9", + "@smithy/middleware-retry": "^4.1.10", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.1", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.17", + "@smithy/util-defaults-mode-node": "^4.0.17", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.823.0.tgz", + "integrity": "sha512-1Cf4w8J7wYexz0KU3zpaikHvldGXQEjFldHOhm0SBGRy7qfYNXecfJAamccF7RdgLxKGgkv5Pl9zX/Z/DcW9zg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.5.1", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.1", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.823.0.tgz", + "integrity": "sha512-AIrLLwumObge+U1klN4j5ToIozI+gE9NosENRyHe0GIIZgTLOG/8jxrMFVYFeNHs7RUtjDTxxewislhFyGxJ/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.823.0.tgz", + "integrity": "sha512-u4DXvB/J/o2bcvP1JP6n3ch7V3/NngmiJFPsM0hKUyRlLuWM37HEDEdjPRs3/uL/soTxrEhWKTA9//YVkvzI0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.1", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.823.0.tgz", + "integrity": "sha512-C0o63qviK5yFvjH9zKWAnCUBkssJoQ1A1XAHe0IAQkurzoNBSmu9oVemqwnKKHA4H6QrmusaEERfL00yohIkJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.823.0", + "@aws-sdk/credential-provider-env": "3.823.0", + "@aws-sdk/credential-provider-http": "3.823.0", + "@aws-sdk/credential-provider-process": "3.823.0", + "@aws-sdk/credential-provider-sso": "3.823.0", + "@aws-sdk/credential-provider-web-identity": "3.823.0", + "@aws-sdk/nested-clients": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.823.0.tgz", + "integrity": "sha512-nfSxXVuZ+2GJDpVFlflNfh55Yb4BtDsXLGNssXF5YU6UgSPsi8j2YkaE92Jv2s7dlUK07l0vRpLyPuXMaGeiRQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.823.0", + "@aws-sdk/credential-provider-http": "3.823.0", + "@aws-sdk/credential-provider-ini": "3.823.0", + "@aws-sdk/credential-provider-process": "3.823.0", + "@aws-sdk/credential-provider-sso": "3.823.0", + "@aws-sdk/credential-provider-web-identity": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.823.0.tgz", + "integrity": "sha512-U/A10/7zu2FbMFFVpIw95y0TZf+oYyrhZTBn9eL8zgWcrYRqxrxdqtPj/zMrfIfyIvQUhuJSENN4dx4tfpCMWQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.823.0.tgz", + "integrity": "sha512-ff8IM80Wqz1V7VVMaMUqO2iR417jggfGWLPl8j2l7uCgwpEyop1ZZl5CFVYEwSupRBtwp+VlW1gTCk7ke56MUw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.823.0", + "@aws-sdk/core": "3.823.0", + "@aws-sdk/token-providers": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.823.0.tgz", + "integrity": "sha512-lzoZdJMQq9w7i4lXVka30cVBe/dZoUDZST8Xz/soEd73gg7RTKgG+0szL4xFWgdBDgcJDWLfZfJzlbyIVyAyOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.823.0", + "@aws-sdk/nested-clients": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.821.0.tgz", + "integrity": "sha512-cebgeytKlWOgGczLo3BPvNY9XlzAzGZQANSysgJ2/8PSldmUpXRIF+GKPXDVhXeInWYHIfB8zZi3RqrPoXcNYQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.821.0.tgz", + "integrity": "sha512-zAOoSZKe1njOrtynvK6ZORU57YGv5I7KP4+rwOvUN3ZhJbQ7QPf8gKtFUCYAPRMegaXCKF/ADPtDZBAmM+zZ9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.823.0.tgz", + "integrity": "sha512-Elt6G1ryEEdkrppqbyJON0o2x4x9xKknimJtMLdfG1b4YfO9X+UB31pk4R2SHvMYfrJ+p8DE2jRAhvV4g/dwIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", + "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.821.0.tgz", + "integrity": "sha512-sKrm80k0t3R0on8aA/WhWFoMaAl4yvdk+riotmMElLUpcMcRXAd1+600uFVrxJqZdbrKQ0mjX0PjT68DlkYXLg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", + "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", + "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.823.0.tgz", + "integrity": "sha512-UV755wt2HDru8PbxLn2S0Fvwgdn9mYamexn31Q6wyUGQ6rkpjKNEzL+oNDGQQmDQAOcQO+nLubKFsCwtBM02fQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/core": "^3.5.1", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.1", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.821.0.tgz", + "integrity": "sha512-YYi1Hhr2AYiU/24cQc8HIB+SWbQo6FBkMYojVuz/zgrtkFmALxENGF/21OPg7f/QWd+eadZJRxCjmRwh5F2Cxg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.823.0.tgz", + "integrity": "sha512-TKRQK09ld1LrIPExC9rIDpqnMsWcv+eq8ABKFHVo8mDLTSuWx/IiQ4eCh9T5zDuEZcLY4nNYCSzXKqw6XKcMCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.821.0", + "@smithy/core": "^3.5.1", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.823.0.tgz", + "integrity": "sha512-/BcyOBubrJnd2gxlbbmNJR1w0Z3OVN/UE8Yz20e+ou+Mijjv7EbtVwmWvio1e3ZjphwdA8tVfPYZKwXmrvHKmQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.823.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.823.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.821.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.823.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.1", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.9", + "@smithy/middleware-retry": "^4.1.10", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.1", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.17", + "@smithy/util-defaults-mode-node": "^4.0.17", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", + "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.824.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.824.0.tgz", + "integrity": "sha512-HBjuWeN6Z1pvJjUvGXdMNLwEypKKB4km6zXj9jsbOOwP8NTL6J5rY+JmlX/mfBTmvzmI0kMu2bxlQ4ME2CIRbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.823.0.tgz", + "integrity": "sha512-vz6onCb/+g4y+owxGGPMEMdN789dTfBOgz/c9pFv0f01840w9Rrt46l+gjQlnXnx+0KG6wNeBIVhFdbCfV3HyQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.823.0", + "@aws-sdk/nested-clients": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.821.0.tgz", + "integrity": "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", + "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.821.0.tgz", + "integrity": "sha512-Uknt/zUZnLE76zaAAPEayOeF5/4IZ2puTFXvcSCWHsi9m3tqbb9UozlnlVqvCZLCRWfQryZQoG2W4XSS3qgk5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", + "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.823.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.823.0.tgz", + "integrity": "sha512-WvNeRz7HV3JLBVGTXW4Qr5QvvWY0vtggH5jW/NqHFH+ZEliVQaUIJ/HNLMpMoCSiu/DlpQAyAjRZXAptJ0oqbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.823.0", + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", + "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz", + "integrity": "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", + "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", + "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", + "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.4.tgz", + "integrity": "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.4.tgz", + "integrity": "sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.2.tgz", + "integrity": "sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.4.tgz", + "integrity": "sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.4.tgz", + "integrity": "sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", + "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.4.tgz", + "integrity": "sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^4.0.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.4.tgz", + "integrity": "sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.4.tgz", + "integrity": "sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.11.tgz", + "integrity": "sha512-zDogwtRLzKl58lVS8wPcARevFZNBOOqnmzWWxVe9XiaXU2CADFjvJ9XfNibgkOWs08sxLuSr81NrpY4mgp9OwQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.12.tgz", + "integrity": "sha512-wvIH70c4e91NtRxdaLZF+mbLZ/HcC6yg7ySKUiufL6ESp6zJUSnJucZ309AvG9nqCFHSRB5I6T3Ez1Q9wCh0Ww==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.5", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.5.tgz", + "integrity": "sha512-LvcfhrnCBvCmTee81pRlh1F39yTS/+kYleVeLCwNtkY8wtGg8V/ca9rbZZvYIl8OjlMtL6KIjaiL/lgVqHD2nA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.3.tgz", + "integrity": "sha512-xxzNYgA0HD6ETCe5QJubsxP0hQH3QK3kbpJz3QrosBCuIWyEXLR/CO5hFb2OeawEKUxMNhz3a1nuJNN2np2RMA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.19.tgz", + "integrity": "sha512-mvLMh87xSmQrV5XqnUYEPoiFFeEGYeAKIDDKdhE2ahqitm8OHM3aSvhqL6rrK6wm1brIk90JhxDf5lf2hbrLbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.19.tgz", + "integrity": "sha512-8tYnx+LUfj6m+zkUUIrIQJxPM1xVxfRBvoGHua7R/i6qAxOMjqR6CpEpDwKoIs1o0+hOjGvkKE23CafKL0vJ9w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.5.tgz", + "integrity": "sha512-V7MSjVDTlEt/plmOFBn1762Dyu5uqMrV2Pl2X0dYk4XvWfdWJNe9Bs5Bzb56wkCuiWjSfClVMGcsuKrGj7S/yg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.5", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", + "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.5.tgz", + "integrity": "sha512-4QvC49HTteI1gfemu0I1syWovJgPvGn7CVUoN9ZFkdvr/cCFkrEL7qNCdx/2eICqDWEGnnr68oMdSIPCLAriSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@tensorflow-models/universal-sentence-encoder": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@tensorflow-models/universal-sentence-encoder/-/universal-sentence-encoder-1.3.3.tgz", + "integrity": "sha512-mipL7ad0CW6uQ68FUkNgkNj/zgA4qgBnNcnMMkNTdL9MUMnzCxu3AE8pWnx2ReKHwdqEG4e8IpaYKfH4B8bojg==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-converter": "^3.6.0", + "@tensorflow/tfjs-core": "^3.6.0" + } + }, + "node_modules/@tensorflow/tfjs": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.22.0.tgz", + "integrity": "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@tensorflow/tfjs-backend-webgl": "4.22.0", + "@tensorflow/tfjs-converter": "4.22.0", + "@tensorflow/tfjs-core": "4.22.0", + "@tensorflow/tfjs-data": "4.22.0", + "@tensorflow/tfjs-layers": "4.22.0", + "argparse": "^1.0.10", + "chalk": "^4.1.0", + "core-js": "3.29.1", + "regenerator-runtime": "^0.13.5", + "yargs": "^16.0.3" + }, + "bin": { + "tfjs-custom-module": "dist/tools/custom_module/cli.js" + } + }, + "node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", + "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", + "license": "Apache-2.0", + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-backend-webgl": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", + "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-converter": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", + "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", + "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.7.0", + "@types/seedrandom": "^2.4.28", + "@webgpu/types": "0.1.38", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs-data": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz", + "integrity": "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==", + "license": "Apache-2.0", + "dependencies": { + "@types/node-fetch": "^2.1.2", + "node-fetch": "~2.6.1", + "string_decoder": "^1.3.0" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0", + "seedrandom": "^3.0.5" + } + }, + "node_modules/@tensorflow/tfjs-layers": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz", + "integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==", + "license": "Apache-2.0 AND MIT", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.57", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", + "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.3.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", + "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==", + "license": "MIT" + }, + "node_modules/@types/omelette": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@types/omelette/-/omelette-0.4.5.tgz", + "integrity": "sha512-zUCJpVRwfMcZfkxSCGp73mgd3/xesvPz5tQJIORlfP/zkYEyp9KUfF7IP3RRjyZR3DwxkPs96/IFf70GmYZYHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/seedrandom": { + "version": "2.4.34", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", + "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==", + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webgpu/types": { + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", + "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001721", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", + "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", + "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.165", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", + "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", + "dev": true, + "license": "ISC" + }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gh-pages": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^11.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/omelette": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/omelette/-/omelette-0.4.17.tgz", + "integrity": "sha512-UlU69G6Bhu0XFjw3tjFZ0qyiMUjAOR+rdzblA1nLQ8xiqFtxOVlkhM39BlgTpLFx9fxkm6rnxNNRsS5GxE/yww==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", + "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.44.0", + "@rollup/rollup-android-arm64": "4.44.0", + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-freebsd-arm64": "4.44.0", + "@rollup/rollup-freebsd-x64": "4.44.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", + "@rollup/rollup-linux-arm-musleabihf": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-musl": "4.44.0", + "@rollup/rollup-linux-s390x-gnu": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-ia32-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.0.tgz", + "integrity": "sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.3.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", + "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..25258753 --- /dev/null +++ b/package.json @@ -0,0 +1,154 @@ +{ + "name": "@soulcraft/brainy", + "version": "0.9.10", + "description": "A vector graph database using HNSW indexing with Origin Private File System storage", + "main": "dist/unified.js", + "module": "dist/unified.js", + "types": "dist/unified.d.ts", + "type": "module", + "sideEffects": false, + "exports": { + ".": { + "import": "./dist/unified.js", + "types": "./dist/unified.d.ts" + }, + "./min": { + "import": "./dist/unified.min.js" + }, + "./types/graphTypes": { + "import": "./dist/types/graphTypes.js", + "types": "./dist/types/graphTypes.d.ts" + }, + "./types/augmentations": { + "import": "./dist/types/augmentations.js", + "types": "./dist/types/augmentations.d.ts" + } + }, + "engines": { + "node": ">=23.11.0" + }, + "scripts": { + "prebuild": "node scripts/generate-version.js", + "build": "BUILD_TYPE=unified rollup -c rollup.config.js", + "build:browser": "BUILD_TYPE=browser rollup -c rollup.config.js", + "start": "node dist/unified.js", + "demo": "npm run build && npm run build:browser && npx http-server -o /index.html", + "cli": "node ./cli-wrapper.js", + "version": "node scripts/generate-version.js", + "version:patch": "npm version patch", + "version:minor": "npm version minor", + "version:major": "npm version major", + "deploy": "npm run build && npm publish", + "deploy:cloud:aws": "cd cloud-wrapper && npm run build && npm run deploy:aws", + "deploy:cloud:gcp": "cd cloud-wrapper && npm run build && npm run deploy:gcp", + "deploy:cloud:cloudflare": "cd cloud-wrapper && npm run build && npm run deploy:cloudflare", + "deploy:cloud": "echo 'Please use one of the following commands to deploy to a specific cloud provider:' && echo ' npm run deploy:cloud:aws' && echo ' npm run deploy:cloud:gcp' && echo ' npm run deploy:cloud:cloudflare'", + "postinstall": "echo 'Note: If you encounter dependency conflicts with TensorFlow.js packages, please use: npm install --legacy-peer-deps'" + }, + "bin": { + "brainy": "cli-wrapper.js" + }, + "keywords": [ + "vector-database", + "hnsw", + "opfs", + "origin-private-file-system", + "embeddings", + "graph-database", + "streaming-data" + ], + "author": "David Snelling (david@soulcraft.com)", + "license": "MIT", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/soulcraft-research/brainy", + "bugs": { + "url": "https://github.com/soulcraft-research/brainy/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/soulcraft-research/brainy.git" + }, + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "dist/types/", + "LICENSE", + "README.md", + "cli-wrapper.js", + "brainy.png" + ], + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-typescript": "^11.1.6", + "@types/node": "^20.4.5", + "@types/omelette": "^0.4.5", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.45.0", + "rollup": "^4.12.0", + "rollup-plugin-terser": "^7.0.2", + "tslib": "^2.8.1", + "typescript": "^5.1.6" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.427.0", + "@tensorflow-models/universal-sentence-encoder": "^1.3.3", + "@tensorflow/tfjs": "^4.22.0", + "@tensorflow/tfjs-backend-cpu": "^4.22.0", + "@tensorflow/tfjs-converter": "^4.22.0", + "@tensorflow/tfjs-core": "^4.22.0", + "@tensorflow/tfjs-layers": "^4.22.0", + "buffer": "^6.0.3", + "commander": "^14.0.0", + "omelette": "^0.4.17", + "uuid": "^9.0.0" + }, + "prettier": { + "arrowParens": "always", + "bracketSameLine": true, + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "css", + "printWidth": 80, + "proseWrap": "preserve", + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false + }, + "eslintConfig": { + "root": true, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "after-used", + "argsIgnorePattern": "^_" + } + ], + "semi": "off", + "@typescript-eslint/semi": [ + "error", + "never" + ], + "no-extra-semi": "off" + } + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..3b34c456 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,179 @@ +import typescript from '@rollup/plugin-typescript' +import resolve from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' +import json from '@rollup/plugin-json' +import { terser } from 'rollup-plugin-terser' +import replace from '@rollup/plugin-replace' + +// Custom plugin to provide empty shims for Node.js built-in modules in browser environments +const nodeModuleShims = () => { + return { + name: 'node-module-shims', + resolveId(source) { + // List of Node.js built-in modules to shim + const nodeBuiltins = ['fs', 'path', 'util', 'child_process'] + + if (nodeBuiltins.includes(source)) { + // Return a virtual module ID for the shim + return `\0${source}-shim` + } + return null + }, + load(id) { + // If this is one of our shims, return an empty module + if (id.startsWith('\0') && id.endsWith('-shim')) { + console.log( + `Providing empty shim for Node.js module: ${id.slice(1, -5)}` + ) + return 'export default {}; export const promises = {};' + } + return null + } + } +} + +// Custom plugin to provide Buffer polyfill for browser environments +const bufferPolyfill = () => { + return { + name: 'buffer-polyfill', + // This will run before the bundle is generated + buildStart() { + console.log('Setting up Buffer polyfill for the bundle') + }, + // This will run for each module + transform(code, id) { + // Only transform the entry point + if (id.includes('src/unified.ts')) { + // Add import for buffer at the top of the file + const bufferImport = ` +// Import Buffer polyfill +import { Buffer as BufferPolyfill } from 'buffer'; + +// Make Buffer available globally +if (typeof window !== 'undefined' && typeof globalThis.Buffer === 'undefined') { + globalThis.Buffer = BufferPolyfill; +} +`; + return { + code: bufferImport + code, + map: { mappings: '' } // Provide an empty sourcemap to avoid warnings + }; + } + return null; // Return null to let Rollup handle other files normally + } + } +} + +// Custom plugin to fix 'this' references in specific files +const fixThisReferences = () => { + return { + name: 'fix-this-references', + transform(code, id) { + // Only transform the specific files that have issues with 'this' + if ( + id.includes( + '@tensorflow/tfjs-layers/dist/layers/convolutional_recurrent.js' + ) + ) { + // Replace 'this' with 'globalThis' in the problematic code + return { + code: code.replace(/\bthis\b/g, 'globalThis'), + map: { mappings: '' } // Provide an empty sourcemap to avoid warnings + } + } + return null // Return null to let Rollup handle other files normally + } + } +} + +// Get build type from environment variable or default to 'unified' +const buildType = process.env.BUILD_TYPE || 'unified' + +// Configuration based on build type +const config = { + unified: { + input: 'src/unified.ts', + outputPrefix: 'unified', + tsconfig: './tsconfig.unified.json', + declaration: true, + declarationMap: true, + intro: ` +// Buffer polyfill is now included via the buffer-polyfill plugin + ` + }, + browser: { + input: 'src/unified.ts', + outputPrefix: 'brainy', + tsconfig: './tsconfig.browser.json', + declaration: false, + declarationMap: false, + intro: ` +// Set global for compatibility +var global = typeof window !== "undefined" ? window : this; +// Buffer polyfill is now included via the buffer-polyfill plugin +` + } +} + +// Get configuration for the current build type +const buildConfig = config[buildType] + +// Create the rollup configuration +export default { + input: buildConfig.input, + output: [ + { + dir: 'dist', + entryFileNames: `${buildConfig.outputPrefix}.js`, + format: 'es', + sourcemap: true, + inlineDynamicImports: true, + intro: buildConfig.intro + }, + { + dir: 'dist', + entryFileNames: `${buildConfig.outputPrefix}.min.js`, + format: 'es', + sourcemap: true, + inlineDynamicImports: true, + intro: buildConfig.intro, + plugins: [terser()] + } + ], + plugins: [ + // Add environment replacement for unified build + ...(buildType === 'unified' + ? [ + replace({ + preventAssignment: true, + 'process.env.NODE_ENV': JSON.stringify('production') + }) + ] + : []), + // Add our custom plugins + fixThisReferences(), + nodeModuleShims(), + bufferPolyfill(), // Add Buffer polyfill + resolve({ + browser: true, + preferBuiltins: false + }), + commonjs({ + transformMixedEsModules: true + }), + json(), + typescript({ + tsconfig: buildConfig.tsconfig, + declaration: buildConfig.declaration, + declarationMap: buildConfig.declarationMap + }) + ], + external: [ + // Add any dependencies you want to exclude from the bundle + '@aws-sdk/client-s3', + '@smithy/util-stream', + '@smithy/node-http-handler', + '@aws-crypto/crc32c', + 'node:stream/web' + ] +} diff --git a/scalingStrategy.md b/scalingStrategy.md new file mode 100644 index 00000000..b1d66477 --- /dev/null +++ b/scalingStrategy.md @@ -0,0 +1,77 @@ + + +# HNSW and Large-Scale Data Management + +HNSW (Hierarchical Navigable Small World) is a graph-based algorithm for approximate nearest neighbor search that is designed to be efficient and scalable. However, when dealing with datasets that can't fit entirely in memory (like terabytes of data), there are important considerations and adaptations needed. + +## Standard HNSW Implementation Limitations + +Looking at the implementation in this project, the standard HNSW approach has some memory limitations: + +1. **In-Memory Index**: The core `HNSWIndex` class keeps all nodes and their connections in memory: + ```typescript + private nouns: Map = new Map() + ``` + +2. **Full Load During Initialization**: During initialization, all nodes are loaded from storage into memory: + ```typescript + // Load all nouns from storage + const nouns: HNSWNoun[] = await this.storage!.getAllNouns() + + // Clear the index and add all nouns + this.index.clear() + for (const noun of nouns) { + // Add to index + this.index.addItem({ + id: noun.id, + vector: noun.vector + }) + } + ``` + +## Approaches for Terabyte-Scale Data + +For terabyte-scale data that can't fit in memory, several approaches can be used: + +### 1. Disk-Based HNSW + +Modified HNSW implementations can use disk-based storage with intelligent caching: + +- **Partial Loading**: Only load the most frequently accessed parts of the graph into memory +- **Page-Based Access**: Organize the graph into pages that can be swapped in and out of memory +- **Memory-Mapped Files**: Use memory-mapped files to let the OS handle paging + +### 2. Distributed HNSW + +For truly massive datasets, a distributed approach is necessary: + +- **Sharding**: Partition the vector space and distribute across multiple machines +- **Hierarchical Search**: Use a coarse quantization layer to route queries to the right shard +- **Federated Results**: Combine results from multiple shards + +### 3. Hybrid Solutions + +Practical implementations often combine multiple techniques: + +- **Quantization**: Reduce vector precision (e.g., from 32-bit to 8-bit) to fit more vectors in memory +- **Product Quantization**: Compress vectors while maintaining search accuracy +- **Two-Tier Architecture**: Use a small in-memory index to route to larger disk-based indices + +## Real-World Examples + +Several systems implement HNSW for large-scale data: + +- **Qdrant and Milvus**: Vector databases that support disk-based HNSW indices +- **FAISS**: Facebook's similarity search library with HNSW implementation that supports GPU and distributed setups +- **DiskANN**: Microsoft's disk-based approximate nearest neighbor search system + +## Conclusion + +While the basic HNSW algorithm requires the graph structure to be in memory for optimal performance, modified implementations can handle terabyte-scale data through: + +1. Disk-based storage with efficient caching +2. Distributed architectures across multiple machines +3. Vector compression techniques +4. Hierarchical multi-tier approaches + +These adaptations allow HNSW to scale to massive datasets while maintaining reasonable query performance, though typically with some trade-offs in terms of search accuracy or latency compared to a fully in-memory implementation. \ No newline at end of file diff --git a/scripts/encode-image.js b/scripts/encode-image.js new file mode 100644 index 00000000..bb9e1dfb --- /dev/null +++ b/scripts/encode-image.js @@ -0,0 +1,47 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Get the directory of the current module +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Path to the image file +const imagePath = path.join(__dirname, '..', 'brainy.png'); + +// Read the image file +const imageBuffer = fs.readFileSync(imagePath); + +// Convert the image to base64 +const base64Image = imageBuffer.toString('base64'); + +// Get the MIME type based on file extension +const getMimeType = (filePath) => { + const ext = path.extname(filePath).toLowerCase(); + switch (ext) { + case '.png': + return 'image/png'; + case '.jpg': + case '.jpeg': + return 'image/jpeg'; + case '.gif': + return 'image/gif'; + case '.svg': + return 'image/svg+xml'; + default: + return 'application/octet-stream'; + } +}; + +const mimeType = getMimeType(imagePath); + +// Create the data URL +const dataUrl = `data:${mimeType};base64,${base64Image}`; + +// Output the complete HTML img tag +const imgTag = `Brainy Logo`; + +// Write to a file instead of console.log to avoid truncation +fs.writeFileSync(path.join(__dirname, '..', 'encoded-image.html'), imgTag); + +console.log('Base64 encoded image has been saved to encoded-image.html'); diff --git a/scripts/generate-version.js b/scripts/generate-version.js new file mode 100755 index 00000000..a155ea83 --- /dev/null +++ b/scripts/generate-version.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/** + * Generate Version Script + * + * This script generates a version.js file that exports the version from package.json. + * This allows the CLI to access the version without having to read package.json directly, + * which can be problematic when the package is installed globally. + * + * It also updates the version in the README.md file to ensure it stays in sync with package.json. + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +// Get the directory of the current module +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// Path to the root directory +const rootDir = path.join(__dirname, '..') + +// Path to package.json +const packageJsonPath = path.join(rootDir, 'package.json') + +// Path to the output directory +const outputDir = path.join(rootDir, 'src', 'utils') + +// Path to the output file +const outputFile = path.join(outputDir, 'version.ts') + +// Path to README.md +const readmePath = path.join(rootDir, 'README.md') + +// Read package.json +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) +const version = packageJson.version + +// Create the output directory if it doesn't exist +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) +} + +// Generate the version.ts file +const content = `/** + * This file is auto-generated during the build process. + * Do not modify this file directly. + */ + +export const VERSION = '${version}'; +` + +// Write the file +fs.writeFileSync(outputFile, content) + +console.log(`Generated version.ts with version ${version}`) + +// Update README.md with the current version, Node.js version, and TypeScript version +try { + let readmeContent = fs.readFileSync(readmePath, 'utf8') + + // Get Node.js version requirement from package.json + const nodeVersion = packageJson.engines.node.replace('>=', '') + + // Get TypeScript version from package.json devDependencies + const typescriptVersion = packageJson.devDependencies.typescript.replace('^', '') + + // Update npm badge - using a more flexible approach with explicit version + const npmBadgeRegex = /\[\!\[npm\].*?\]\(https:\/\/www\.npmjs\.com\/package\/@soulcraft\/brainy\)/g + if (npmBadgeRegex.test(readmeContent)) { + readmeContent = readmeContent.replace( + npmBadgeRegex, + `[![npm](https://img.shields.io/badge/npm-v${version}-blue.svg)](https://www.npmjs.com/package/@soulcraft/brainy)` + ) + } else { + console.log('Warning: Could not find npm badge in README.md') + } + + // Update Node.js badge - using a more flexible approach + const nodeBadgeRegex = /\[\!\[Node\.js\].*?\]\(https:\/\/nodejs\.org\/\)/g + if (nodeBadgeRegex.test(readmeContent)) { + readmeContent = readmeContent.replace( + nodeBadgeRegex, + `[![Node.js](https://img.shields.io/badge/node-%3E%3D${nodeVersion}-brightgreen.svg)](https://nodejs.org/)` + ) + } else { + console.log('Warning: Could not find Node.js badge in README.md') + } + + // Update TypeScript badge - using a more flexible approach + const tsBadgeRegex = /\[\!\[TypeScript\].*?\]\(https:\/\/www\.typescriptlang\.org\/\)/g + if (tsBadgeRegex.test(readmeContent)) { + readmeContent = readmeContent.replace( + tsBadgeRegex, + `[![TypeScript](https://img.shields.io/badge/TypeScript-${typescriptVersion}-blue.svg)](https://www.typescriptlang.org/)` + ) + } else { + console.log('Warning: Could not find TypeScript badge in README.md') + } + + // Write the updated README back to disk + fs.writeFileSync(readmePath, readmeContent) + console.log(`Updated README.md with npm version ${version}, Node.js version ${nodeVersion}, and TypeScript version ${typescriptVersion}`) +} catch (error) { + console.error('Error updating README.md:', error) +} diff --git a/scripts/update-readme.js b/scripts/update-readme.js new file mode 100644 index 00000000..ac369338 --- /dev/null +++ b/scripts/update-readme.js @@ -0,0 +1,26 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Get the directory of the current module +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Paths to the files +const readmePath = path.join(__dirname, '..', 'README.md'); +const encodedImagePath = path.join(__dirname, '..', 'encoded-image.html'); + +// Read the files +const readmeContent = fs.readFileSync(readmePath, 'utf8'); +const encodedImageTag = fs.readFileSync(encodedImagePath, 'utf8'); + +// Replace the image tag in the README +const updatedReadme = readmeContent.replace( + /Brainy Logo/, + encodedImageTag +); + +// Write the updated README +fs.writeFileSync(readmePath, updatedReadme); + +console.log('README.md has been updated with the base64-encoded image.'); diff --git a/src/augmentationFactory.ts b/src/augmentationFactory.ts new file mode 100644 index 00000000..1e77f070 --- /dev/null +++ b/src/augmentationFactory.ts @@ -0,0 +1,628 @@ +/** + * Augmentation Factory + * + * This module provides a simplified factory for creating augmentations with minimal boilerplate. + * It reduces the complexity of creating and using augmentations by providing a fluent API + * and handling common patterns automatically. + */ + +import { + IAugmentation, + AugmentationType, + AugmentationResponse, + ISenseAugmentation, + IConduitAugmentation, + ICognitionAugmentation, + IMemoryAugmentation, + IPerceptionAugmentation, + IDialogAugmentation, + IActivationAugmentation, + IWebSocketSupport, + WebSocketConnection +} from './types/augmentations.js' +import { registerAugmentation } from './augmentationRegistry.js' + +/** + * Options for creating an augmentation + */ +export interface AugmentationOptions { + name: string + description?: string + enabled?: boolean + autoRegister?: boolean + autoInitialize?: boolean +} + +/** + * Base class for all augmentations created with the factory + * Handles common functionality like initialization, shutdown, and status + */ +class BaseAugmentation implements IAugmentation { + readonly name: string + readonly description: string + enabled: boolean = true + protected isInitialized: boolean = false + + constructor(options: AugmentationOptions) { + this.name = options.name + this.description = options.description || `${options.name} augmentation` + this.enabled = options.enabled !== false + } + + async initialize(): Promise { + if (this.isInitialized) return + this.isInitialized = true + } + + async shutDown(): Promise { + this.isInitialized = false + } + + async getStatus(): Promise<'active' | 'inactive' | 'error'> { + return this.isInitialized ? 'active' : 'inactive' + } + + protected async ensureInitialized(): Promise { + if (!this.isInitialized) { + await this.initialize() + } + } +} + +/** + * Factory for creating sense augmentations + */ +export function createSenseAugmentation( + options: AugmentationOptions & { + processRawData?: ( + rawData: Buffer | string, + dataType: string + ) => + | Promise> + | AugmentationResponse<{ + nouns: string[] + verbs: string[] + }> + listenToFeed?: ( + feedUrl: string, + callback: (data: { nouns: string[]; verbs: string[] }) => void + ) => Promise + } +): ISenseAugmentation { + const augmentation = new BaseAugmentation(options) as unknown as ISenseAugmentation + + // Implement the sense augmentation methods + augmentation.processRawData = async ( + rawData: Buffer | string, + dataType: string + ) => { + await augmentation.ensureInitialized() + + if (options.processRawData) { + const result = options.processRawData(rawData, dataType) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: { nouns: [], verbs: [] }, + error: 'processRawData not implemented' + } + } + + augmentation.listenToFeed = async ( + feedUrl: string, + callback: (data: { nouns: string[]; verbs: string[] }) => void + ) => { + await augmentation.ensureInitialized() + + if (options.listenToFeed) { + return options.listenToFeed(feedUrl, callback) + } + + throw new Error('listenToFeed not implemented') + } + + // Auto-register if requested + if (options.autoRegister) { + registerAugmentation(augmentation) + + // Auto-initialize if requested + if (options.autoInitialize) { + augmentation.initialize().catch((error) => { + console.error( + `Failed to initialize augmentation ${augmentation.name}:`, + error + ) + }) + } + } + + return augmentation +} + +/** + * Factory for creating conduit augmentations + */ +export function createConduitAugmentation( + options: AugmentationOptions & { + establishConnection?: ( + targetSystemId: string, + config: Record + ) => + | Promise> + | AugmentationResponse + readData?: ( + query: Record, + options?: Record + ) => Promise> | AugmentationResponse + writeData?: ( + data: Record, + options?: Record + ) => Promise> | AugmentationResponse + monitorStream?: ( + streamId: string, + callback: (data: unknown) => void + ) => Promise + } +): IConduitAugmentation { + const augmentation = new BaseAugmentation(options) as unknown as IConduitAugmentation + + // Implement the conduit augmentation methods + augmentation.establishConnection = async ( + targetSystemId: string, + config: Record + ) => { + await augmentation.ensureInitialized() + + if (options.establishConnection) { + const result = options.establishConnection(targetSystemId, config) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: null as any, + error: 'establishConnection not implemented' + } + } + + augmentation.readData = async ( + query: Record, + opts?: Record + ) => { + await augmentation.ensureInitialized() + + if (options.readData) { + const result = options.readData(query, opts) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: null, + error: 'readData not implemented' + } + } + + augmentation.writeData = async ( + data: Record, + opts?: Record + ) => { + await augmentation.ensureInitialized() + + if (options.writeData) { + const result = options.writeData(data, opts) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: null, + error: 'writeData not implemented' + } + } + + augmentation.monitorStream = async ( + streamId: string, + callback: (data: unknown) => void + ) => { + await augmentation.ensureInitialized() + + if (options.monitorStream) { + return options.monitorStream(streamId, callback) + } + + throw new Error('monitorStream not implemented') + } + + // Auto-register if requested + if (options.autoRegister) { + registerAugmentation(augmentation) + + // Auto-initialize if requested + if (options.autoInitialize) { + augmentation.initialize().catch((error) => { + console.error( + `Failed to initialize augmentation ${augmentation.name}:`, + error + ) + }) + } + } + + return augmentation +} + +/** + * Factory for creating memory augmentations + */ +export function createMemoryAugmentation( + options: AugmentationOptions & { + storeData?: ( + key: string, + data: unknown, + options?: Record + ) => Promise> | AugmentationResponse + retrieveData?: ( + key: string, + options?: Record + ) => Promise> | AugmentationResponse + updateData?: ( + key: string, + data: unknown, + options?: Record + ) => Promise> | AugmentationResponse + deleteData?: ( + key: string, + options?: Record + ) => Promise> | AugmentationResponse + listDataKeys?: ( + pattern?: string, + options?: Record + ) => + | Promise> + | AugmentationResponse + search?: ( + query: unknown, + k?: number, + options?: Record + ) => + | Promise< + AugmentationResponse< + Array<{ id: string; score: number; data: unknown }> + > + > + | AugmentationResponse< + Array<{ id: string; score: number; data: unknown }> + > + } +): IMemoryAugmentation { + const augmentation = new BaseAugmentation(options) as unknown as IMemoryAugmentation + + // Implement the memory augmentation methods + augmentation.storeData = async ( + key: string, + data: unknown, + opts?: Record + ) => { + await augmentation.ensureInitialized() + + if (options.storeData) { + const result = options.storeData(key, data, opts) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: false, + error: 'storeData not implemented' + } + } + + augmentation.retrieveData = async ( + key: string, + opts?: Record + ) => { + await augmentation.ensureInitialized() + + if (options.retrieveData) { + const result = options.retrieveData(key, opts) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: null, + error: 'retrieveData not implemented' + } + } + + augmentation.updateData = async ( + key: string, + data: unknown, + opts?: Record + ) => { + await augmentation.ensureInitialized() + + if (options.updateData) { + const result = options.updateData(key, data, opts) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: false, + error: 'updateData not implemented' + } + } + + augmentation.deleteData = async ( + key: string, + opts?: Record + ) => { + await augmentation.ensureInitialized() + + if (options.deleteData) { + const result = options.deleteData(key, opts) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: false, + error: 'deleteData not implemented' + } + } + + augmentation.listDataKeys = async ( + pattern?: string, + opts?: Record + ) => { + await augmentation.ensureInitialized() + + if (options.listDataKeys) { + const result = options.listDataKeys(pattern, opts) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: [], + error: 'listDataKeys not implemented' + } + } + + augmentation.search = async ( + query: unknown, + k?: number, + opts?: Record + ) => { + await augmentation.ensureInitialized() + + if (options.search) { + const result = options.search(query, k, opts) + return result instanceof Promise ? await result : result + } + + return { + success: false, + data: [], + error: 'search not implemented' + } + } + + // Auto-register if requested + if (options.autoRegister) { + registerAugmentation(augmentation) + + // Auto-initialize if requested + if (options.autoInitialize) { + augmentation.initialize().catch((error) => { + console.error( + `Failed to initialize augmentation ${augmentation.name}:`, + error + ) + }) + } + } + + return augmentation +} + +/** + * Factory for creating WebSocket-enabled augmentations + * This can be combined with other augmentation factories to create WebSocket-enabled versions + */ +export function addWebSocketSupport( + augmentation: T, + options: { + connectWebSocket?: ( + url: string, + protocols?: string | string[] + ) => Promise + sendWebSocketMessage?: ( + connectionId: string, + data: unknown + ) => Promise + onWebSocketMessage?: ( + connectionId: string, + callback: (data: unknown) => void + ) => Promise + offWebSocketMessage?: ( + connectionId: string, + callback: (data: unknown) => void + ) => Promise + closeWebSocket?: ( + connectionId: string, + code?: number, + reason?: string + ) => Promise + } +): T & IWebSocketSupport { + const wsAugmentation = augmentation as T & IWebSocketSupport + + // Add WebSocket methods + wsAugmentation.connectWebSocket = async ( + url: string, + protocols?: string | string[] + ) => { + await (augmentation as any).ensureInitialized?.() + + if (options.connectWebSocket) { + return options.connectWebSocket(url, protocols) + } + + throw new Error('connectWebSocket not implemented') + } + + wsAugmentation.sendWebSocketMessage = async ( + connectionId: string, + data: unknown + ) => { + await (augmentation as any).ensureInitialized?.() + + if (options.sendWebSocketMessage) { + return options.sendWebSocketMessage(connectionId, data) + } + + throw new Error('sendWebSocketMessage not implemented') + } + + wsAugmentation.onWebSocketMessage = async ( + connectionId: string, + callback: (data: unknown) => void + ) => { + await (augmentation as any).ensureInitialized?.() + + if (options.onWebSocketMessage) { + return options.onWebSocketMessage(connectionId, callback) + } + + throw new Error('onWebSocketMessage not implemented') + } + + wsAugmentation.offWebSocketMessage = async ( + connectionId: string, + callback: (data: unknown) => void + ) => { + await (augmentation as any).ensureInitialized?.() + + if (options.offWebSocketMessage) { + return options.offWebSocketMessage(connectionId, callback) + } + + throw new Error('offWebSocketMessage not implemented') + } + + wsAugmentation.closeWebSocket = async ( + connectionId: string, + code?: number, + reason?: string + ) => { + await (augmentation as any).ensureInitialized?.() + + if (options.closeWebSocket) { + return options.closeWebSocket(connectionId, code, reason) + } + + throw new Error('closeWebSocket not implemented') + } + + return wsAugmentation +} + +/** + * Simplified function to execute an augmentation method with automatic error handling + * This provides a more concise way to execute augmentation methods compared to the full pipeline + */ +export async function executeAugmentation( + augmentation: IAugmentation, + method: string, + ...args: any[] +): Promise> { + try { + if (!augmentation.enabled) { + return { + success: false, + data: null as any, + error: `Augmentation ${augmentation.name} is disabled` + } + } + + if (typeof (augmentation as any)[method] !== 'function') { + return { + success: false, + data: null as any, + error: `Method ${method} not found on augmentation ${augmentation.name}` + } + } + + const result = await (augmentation as any)[method](...args) + return result + } catch (error) { + console.error(`Error executing ${method} on ${augmentation.name}:`, error) + return { + success: false, + data: null as any, + error: error instanceof Error ? error.message : String(error) + } + } +} + +/** + * Dynamically load augmentations from a module at runtime + * This allows for lazy-loading augmentations when needed instead of at build time + */ +export async function loadAugmentationModule( + modulePromise: Promise, + options: { + autoRegister?: boolean + autoInitialize?: boolean + } = {} +): Promise { + try { + const module = await modulePromise + const augmentations: IAugmentation[] = [] + + // Extract augmentations from the module + for (const key in module) { + const exported = module[key] + + // Skip non-objects and null + if (!exported || typeof exported !== 'object') { + continue + } + + // Check if it's an augmentation + if ( + typeof exported.name === 'string' && + typeof exported.initialize === 'function' && + typeof exported.shutDown === 'function' && + typeof exported.getStatus === 'function' + ) { + augmentations.push(exported) + + // Auto-register if requested + if (options.autoRegister) { + registerAugmentation(exported) + + // Auto-initialize if requested + if (options.autoInitialize) { + exported.initialize().catch((error: Error) => { + console.error( + `Failed to initialize augmentation ${exported.name}:`, + error + ) + }) + } + } + } + } + + return augmentations + } catch (error) { + console.error('Error loading augmentation module:', error) + return [] + } +} diff --git a/src/augmentationPipeline.ts b/src/augmentationPipeline.ts new file mode 100644 index 00000000..f3c5f640 --- /dev/null +++ b/src/augmentationPipeline.ts @@ -0,0 +1,709 @@ +/** + * Augmentation Event Pipeline + * + * This module provides a pipeline for managing and executing multiple augmentations + * of each type. It allows registering multiple augmentations and executing them + * in sequence or in parallel. + */ + +import { + BrainyAugmentations, + IAugmentation, + IWebSocketSupport, + AugmentationResponse, + AugmentationType +} from './types/augmentations.js' +import { isThreadingAvailable, isBrowser, isNode } from './utils/environment.js' +import { executeInThread } from './utils/workerUtils.js' + +/** + * Type definitions for the augmentation registry + */ +type AugmentationRegistry = { + sense: BrainyAugmentations.ISenseAugmentation[]; + conduit: BrainyAugmentations.IConduitAugmentation[]; + cognition: BrainyAugmentations.ICognitionAugmentation[]; + memory: BrainyAugmentations.IMemoryAugmentation[]; + perception: BrainyAugmentations.IPerceptionAugmentation[]; + dialog: BrainyAugmentations.IDialogAugmentation[]; + activation: BrainyAugmentations.IActivationAugmentation[]; + webSocket: IWebSocketSupport[]; +} + +/** + * Execution mode for the pipeline + */ +export enum ExecutionMode { + SEQUENTIAL = 'sequential', + PARALLEL = 'parallel', + FIRST_SUCCESS = 'firstSuccess', + FIRST_RESULT = 'firstResult', + THREADED = 'threaded' // Execute in separate threads when available +} + +/** + * Options for pipeline execution + */ +export interface PipelineOptions { + mode?: ExecutionMode; + timeout?: number; + stopOnError?: boolean; + forceThreading?: boolean; // Force threading even if not in THREADED mode + disableThreading?: boolean; // Disable threading even if in THREADED mode +} + +/** + * Default pipeline options + */ +const DEFAULT_PIPELINE_OPTIONS: PipelineOptions = { + mode: ExecutionMode.SEQUENTIAL, + timeout: 30000, + stopOnError: false, + forceThreading: false, + disableThreading: false +} + +/** + * AugmentationPipeline class + * + * Manages multiple augmentations of each type and provides methods to execute them. + */ +export class AugmentationPipeline { + private registry: AugmentationRegistry = { + sense: [], + conduit: [], + cognition: [], + memory: [], + perception: [], + dialog: [], + activation: [], + webSocket: [] + } + + /** + * Register an augmentation with the pipeline + * + * @param augmentation The augmentation to register + * @returns The pipeline instance for chaining + */ + public register(augmentation: T): AugmentationPipeline { + let registered = false + + // Check for specific augmentation types + if (this.isAugmentationType( + augmentation, + 'processRawData', + 'listenToFeed' + )) { + this.registry.sense.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'establishConnection', + 'readData', + 'writeData', + 'monitorStream' + )) { + this.registry.conduit.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'reason', + 'infer', + 'executeLogic' + )) { + this.registry.cognition.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'storeData', + 'retrieveData', + 'updateData', + 'deleteData', + 'listDataKeys' + )) { + this.registry.memory.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'interpret', + 'organize', + 'generateVisualization' + )) { + this.registry.perception.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'processUserInput', + 'generateResponse', + 'manageContext' + )) { + this.registry.dialog.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'triggerAction', + 'generateOutput', + 'interactExternal' + )) { + this.registry.activation.push(augmentation) + registered = true + } + + // Check if the augmentation supports WebSocket + if (this.isAugmentationType( + augmentation, + 'connectWebSocket', + 'sendWebSocketMessage', + 'onWebSocketMessage', + 'closeWebSocket' + )) { + this.registry.webSocket.push(augmentation as IWebSocketSupport) + registered = true + } + + // If the augmentation wasn't registered as any known type, throw an error + if (!registered) { + throw new Error(`Unknown augmentation type: ${augmentation.name}`) + } + + return this + } + + /** + * Unregister an augmentation from the pipeline + * + * @param augmentationName The name of the augmentation to unregister + * @returns The pipeline instance for chaining + */ + public unregister(augmentationName: string): AugmentationPipeline { + let found = false + + // Remove from all registries + for (const type in this.registry) { + const typedRegistry = this.registry[type as keyof AugmentationRegistry] + const index = typedRegistry.findIndex(aug => aug.name === augmentationName) + + if (index !== -1) { + typedRegistry.splice(index, 1) + found = true + } + } + + return this + } + + /** + * Initialize all registered augmentations + * + * @returns A promise that resolves when all augmentations are initialized + */ + public async initialize(): Promise { + const allAugmentations = this.getAllAugmentations() + + await Promise.all( + allAugmentations.map(augmentation => + augmentation.initialize().catch(error => { + console.error(`Failed to initialize augmentation ${augmentation.name}:`, error) + }) + ) + ) + } + + /** + * Shut down all registered augmentations + * + * @returns A promise that resolves when all augmentations are shut down + */ + public async shutDown(): Promise { + const allAugmentations = this.getAllAugmentations() + + await Promise.all( + allAugmentations.map(augmentation => + augmentation.shutDown().catch(error => { + console.error(`Failed to shut down augmentation ${augmentation.name}:`, error) + }) + ) + ) + } + + /** + * Execute a sense pipeline + * + * @param method The method to execute on each sense augmentation + * @param args The arguments to pass to the method + * @param options The pipeline execution options + * @returns A promise that resolves with the results from all augmentations + */ + public async executeSensePipeline< + M extends keyof BrainyAugmentations.ISenseAugmentation & string, + R extends BrainyAugmentations.ISenseAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.ISenseAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const opts = { ...DEFAULT_PIPELINE_OPTIONS, ...options } + return this.executeTypedPipeline( + this.registry.sense, + method, + args, + opts + ) + } + + /** + * Execute a conduit pipeline + * + * @param method The method to execute on each conduit augmentation + * @param args The arguments to pass to the method + * @param options The pipeline execution options + * @returns A promise that resolves with the results from all augmentations + */ + public async executeConduitPipeline< + M extends keyof BrainyAugmentations.IConduitAugmentation & string, + R extends BrainyAugmentations.IConduitAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IConduitAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const opts = { ...DEFAULT_PIPELINE_OPTIONS, ...options } + return this.executeTypedPipeline( + this.registry.conduit, + method, + args, + opts + ) + } + + /** + * Execute a cognition pipeline + * + * @param method The method to execute on each cognition augmentation + * @param args The arguments to pass to the method + * @param options The pipeline execution options + * @returns A promise that resolves with the results from all augmentations + */ + public async executeCognitionPipeline< + M extends keyof BrainyAugmentations.ICognitionAugmentation & string, + R extends BrainyAugmentations.ICognitionAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.ICognitionAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const opts = { ...DEFAULT_PIPELINE_OPTIONS, ...options } + return this.executeTypedPipeline( + this.registry.cognition, + method, + args, + opts + ) + } + + /** + * Execute a memory pipeline + * + * @param method The method to execute on each memory augmentation + * @param args The arguments to pass to the method + * @param options The pipeline execution options + * @returns A promise that resolves with the results from all augmentations + */ + public async executeMemoryPipeline< + M extends keyof BrainyAugmentations.IMemoryAugmentation & string, + R extends BrainyAugmentations.IMemoryAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IMemoryAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const opts = { ...DEFAULT_PIPELINE_OPTIONS, ...options } + return this.executeTypedPipeline( + this.registry.memory, + method, + args, + opts + ) + } + + /** + * Execute a perception pipeline + * + * @param method The method to execute on each perception augmentation + * @param args The arguments to pass to the method + * @param options The pipeline execution options + * @returns A promise that resolves with the results from all augmentations + */ + public async executePerceptionPipeline< + M extends keyof BrainyAugmentations.IPerceptionAugmentation & string, + R extends BrainyAugmentations.IPerceptionAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IPerceptionAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const opts = { ...DEFAULT_PIPELINE_OPTIONS, ...options } + return this.executeTypedPipeline( + this.registry.perception, + method, + args, + opts + ) + } + + /** + * Execute a dialog pipeline + * + * @param method The method to execute on each dialog augmentation + * @param args The arguments to pass to the method + * @param options The pipeline execution options + * @returns A promise that resolves with the results from all augmentations + */ + public async executeDialogPipeline< + M extends keyof BrainyAugmentations.IDialogAugmentation & string, + R extends BrainyAugmentations.IDialogAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IDialogAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const opts = { ...DEFAULT_PIPELINE_OPTIONS, ...options } + return this.executeTypedPipeline( + this.registry.dialog, + method, + args, + opts + ) + } + + /** + * Execute an activation pipeline + * + * @param method The method to execute on each activation augmentation + * @param args The arguments to pass to the method + * @param options The pipeline execution options + * @returns A promise that resolves with the results from all augmentations + */ + public async executeActivationPipeline< + M extends keyof BrainyAugmentations.IActivationAugmentation & string, + R extends BrainyAugmentations.IActivationAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IActivationAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const opts = { ...DEFAULT_PIPELINE_OPTIONS, ...options } + return this.executeTypedPipeline( + this.registry.activation, + method, + args, + opts + ) + } + + /** + * Get all registered augmentations + * + * @returns An array of all registered augmentations + */ + public getAllAugmentations(): IAugmentation[] { + // Create a Set to avoid duplicates (an augmentation might be in multiple registries) + const allAugmentations = new Set([ + ...this.registry.sense, + ...this.registry.conduit, + ...this.registry.cognition, + ...this.registry.memory, + ...this.registry.perception, + ...this.registry.dialog, + ...this.registry.activation, + ...this.registry.webSocket + ]) + + // Convert back to array + return Array.from(allAugmentations) + } + + /** + * Get all augmentations of a specific type + * + * @param type The type of augmentation to get + * @returns An array of all augmentations of the specified type + */ + public getAugmentationsByType(type: AugmentationType): IAugmentation[] { + switch (type) { + case AugmentationType.SENSE: + return [...this.registry.sense] + case AugmentationType.CONDUIT: + return [...this.registry.conduit] + case AugmentationType.COGNITION: + return [...this.registry.cognition] + case AugmentationType.MEMORY: + return [...this.registry.memory] + case AugmentationType.PERCEPTION: + return [...this.registry.perception] + case AugmentationType.DIALOG: + return [...this.registry.dialog] + case AugmentationType.ACTIVATION: + return [...this.registry.activation] + case AugmentationType.WEBSOCKET: + return [...this.registry.webSocket] + default: + return [] + } + } + + /** + * Get all available augmentation types + * + * @returns An array of all augmentation types that have at least one registered augmentation + */ + public getAvailableAugmentationTypes(): AugmentationType[] { + const availableTypes: AugmentationType[] = [] + + if (this.registry.sense.length > 0) availableTypes.push(AugmentationType.SENSE) + if (this.registry.conduit.length > 0) availableTypes.push(AugmentationType.CONDUIT) + if (this.registry.cognition.length > 0) availableTypes.push(AugmentationType.COGNITION) + if (this.registry.memory.length > 0) availableTypes.push(AugmentationType.MEMORY) + if (this.registry.perception.length > 0) availableTypes.push(AugmentationType.PERCEPTION) + if (this.registry.dialog.length > 0) availableTypes.push(AugmentationType.DIALOG) + if (this.registry.activation.length > 0) availableTypes.push(AugmentationType.ACTIVATION) + if (this.registry.webSocket.length > 0) availableTypes.push(AugmentationType.WEBSOCKET) + + return availableTypes + } + + /** + * Get all WebSocket-supporting augmentations + * + * @returns An array of all augmentations that support WebSocket connections + */ + public getWebSocketAugmentations(): IWebSocketSupport[] { + return [...this.registry.webSocket] + } + + /** + * Check if an augmentation is of a specific type + * + * @param augmentation The augmentation to check + * @param methods The methods that should be present on the augmentation + * @returns True if the augmentation is of the specified type + */ + private isAugmentationType( + augmentation: IAugmentation, + ...methods: (keyof T)[] + ): augmentation is T { + // First check that the augmentation has all the required base methods + const baseMethodsExist = [ + 'initialize', + 'shutDown', + 'getStatus' + ].every(method => typeof (augmentation as any)[method] === 'function') + + if (!baseMethodsExist) { + return false + } + + // Then check that it has all the specific methods for this type + return methods.every(method => typeof (augmentation as any)[method] === 'function') + } + + /** + * Determines if threading should be used based on options and environment + * + * @param options The pipeline options + * @returns True if threading should be used, false otherwise + */ + private shouldUseThreading(options: PipelineOptions): boolean { + // If threading is explicitly disabled, don't use it + if (options.disableThreading) { + return false; + } + + // If threading is explicitly forced, use it if available + if (options.forceThreading) { + return isThreadingAvailable(); + } + + // If in THREADED mode, use threading if available + if (options.mode === ExecutionMode.THREADED) { + return isThreadingAvailable(); + } + + // Otherwise, don't use threading + return false; + } + + /** + * Execute a pipeline for a specific augmentation type + * + * @param augmentations The augmentations to execute + * @param method The method to execute on each augmentation + * @param args The arguments to pass to the method + * @param options The pipeline execution options + * @returns A promise that resolves with the results from all augmentations + */ + private async executeTypedPipeline< + T extends IAugmentation, + M extends keyof T & string, + R extends T[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + augmentations: T[], + method: M & (T[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions + ): Promise[]> { + // Filter out disabled augmentations + const enabledAugmentations = augmentations.filter(aug => aug.enabled !== false) + + if (enabledAugmentations.length === 0) { + return [] + } + + // Create a function to execute the method on an augmentation + const executeMethod = async (augmentation: T): Promise<{ + success: boolean + data: R + error?: string + }> => { + try { + // Create a timeout promise if a timeout is specified + const timeoutPromise = options.timeout + ? new Promise<{ + success: boolean + data: R + error?: string + }>((_, reject) => { + setTimeout(() => { + reject(new Error(`Timeout executing ${String(method)} on ${augmentation.name}`)) + }, options.timeout) + }) + : null + + // Check if threading should be used + const useThreading = this.shouldUseThreading(options); + + // Execute the method on the augmentation, using threading if appropriate + let methodPromise: Promise>; + + if (useThreading) { + // Execute in a separate thread + try { + // Create a function that can be serialized and executed in a worker + const workerFn = (...workerArgs: any[]) => { + // This function will be stringified and executed in the worker + // It needs to be self-contained + const augFn = augmentation[method as string] as Function; + return augFn.apply(augmentation, workerArgs); + }; + + methodPromise = executeInThread>(workerFn.toString(), args); + } catch (threadError) { + console.warn(`Failed to execute in thread, falling back to main thread: ${threadError}`); + // Fall back to executing in the main thread + methodPromise = Promise.resolve((augmentation[method] as Function)(...args) as AugmentationResponse); + } + } else { + // Execute in the main thread + methodPromise = Promise.resolve((augmentation[method] as Function)(...args) as AugmentationResponse); + } + + // Race the method promise against the timeout promise if a timeout is specified + const result = timeoutPromise + ? await Promise.race([methodPromise, timeoutPromise]) + : await methodPromise + + return result + } catch (error) { + console.error(`Error executing ${String(method)} on ${augmentation.name}:`, error) + return { + success: false, + data: null as unknown as R, + error: error instanceof Error ? error.message : String(error) + } + } + } + + // Execute the pipeline based on the specified mode + switch (options.mode) { + case ExecutionMode.PARALLEL: + // Execute all augmentations in parallel + return enabledAugmentations.map(executeMethod) + + case ExecutionMode.THREADED: + // Execute all augmentations in parallel with threading enabled + // Force threading for this mode + const threadedOptions = { ...options, forceThreading: true }; + + // Create a new executeMethod function that uses the threaded options + const executeMethodThreaded = async (augmentation: T) => { + // Save the original options + const originalOptions = options; + + // Set the options to the threaded options + options = threadedOptions; + + // Execute the method + const result = await executeMethod(augmentation); + + // Restore the original options + options = originalOptions; + + return result; + }; + + return enabledAugmentations.map(executeMethodThreaded); + + case ExecutionMode.FIRST_SUCCESS: + // Execute augmentations sequentially until one succeeds + for (const augmentation of enabledAugmentations) { + const resultPromise = executeMethod(augmentation) + const result = await resultPromise + if (result.success) { + return [resultPromise] + } + } + return [] + + case ExecutionMode.FIRST_RESULT: + // Execute augmentations sequentially until one returns a result + for (const augmentation of enabledAugmentations) { + const resultPromise = executeMethod(augmentation) + const result = await resultPromise + if (result.success && result.data) { + return [resultPromise] + } + } + return [] + + case ExecutionMode.SEQUENTIAL: + default: + // Execute augmentations sequentially + const results: Promise<{ + success: boolean + data: R + error?: string + }>[] = [] + for (const augmentation of enabledAugmentations) { + const resultPromise = executeMethod(augmentation) + results.push(resultPromise) + + // Check if we need to stop on error + if (options.stopOnError) { + const result = await resultPromise + if (!result.success) { + break + } + } + } + return results + } + } +} + +// Create and export a default instance of the pipeline +export const augmentationPipeline = new AugmentationPipeline() diff --git a/src/augmentationRegistry.ts b/src/augmentationRegistry.ts new file mode 100644 index 00000000..b8199758 --- /dev/null +++ b/src/augmentationRegistry.ts @@ -0,0 +1,122 @@ +/** + * Augmentation Registry + * + * This module provides a registry for augmentations that are loaded at build time. + * It replaces the dynamic loading mechanism in pluginLoader.ts. + */ + +import { IPipeline } from './types/pipelineTypes.js' +import { AugmentationType, IAugmentation } from './types/augmentations.js' + +// Forward declaration of the pipeline instance to avoid circular dependency +// The actual pipeline will be provided when initializeAugmentationPipeline is called +let defaultPipeline: IPipeline | null = null + +/** + * Sets the default pipeline instance + * This function should be called from pipeline.ts after the pipeline is created + */ +export function setDefaultPipeline(pipeline: IPipeline): void { + defaultPipeline = pipeline +} + +/** + * Registry of all available augmentations + */ +export const availableAugmentations: IAugmentation[] = [] + +/** + * Registers an augmentation with the registry + * + * @param augmentation The augmentation to register + * @returns The augmentation that was registered + */ +export function registerAugmentation(augmentation: T): T { + // Set enabled to true by default if not specified + if (augmentation.enabled === undefined) { + augmentation.enabled = true + } + + // Add to the registry + availableAugmentations.push(augmentation) + + return augmentation +} + +/** + * Initializes the augmentation pipeline with all registered augmentations + * + * @param pipeline Optional custom pipeline to use instead of the default + * @returns The pipeline that was initialized + * @throws Error if no pipeline is provided and the default pipeline hasn't been set + */ +export function initializeAugmentationPipeline( + pipelineInstance?: IPipeline +): IPipeline { + // Use the provided pipeline or fall back to the default + const pipeline = pipelineInstance || defaultPipeline + + if (!pipeline) { + throw new Error('No pipeline provided and default pipeline not set. Call setDefaultPipeline first.') + } + + // Register all augmentations with the pipeline + for (const augmentation of availableAugmentations) { + if (augmentation.enabled) { + pipeline.register(augmentation) + } + } + + return pipeline +} + +/** + * Enables or disables an augmentation by name + * + * @param name The name of the augmentation to enable/disable + * @param enabled Whether to enable or disable the augmentation + * @returns True if the augmentation was found and updated, false otherwise + */ +export function setAugmentationEnabled(name: string, enabled: boolean): boolean { + const augmentation = availableAugmentations.find(aug => aug.name === name) + + if (augmentation) { + augmentation.enabled = enabled + return true + } + + return false +} + +/** + * Gets all augmentations of a specific type + * + * @param type The type of augmentation to get + * @returns An array of all augmentations of the specified type + */ +export function getAugmentationsByType(type: AugmentationType): IAugmentation[] { + return availableAugmentations.filter(aug => { + // Check if the augmentation is of the specified type + // This is a simplified check and may need to be updated based on how types are determined + switch (type) { + case AugmentationType.SENSE: + return 'processRawData' in aug && 'listenToFeed' in aug + case AugmentationType.CONDUIT: + return 'establishConnection' in aug && 'readData' in aug && 'writeData' in aug + case AugmentationType.COGNITION: + return 'reason' in aug && 'infer' in aug && 'executeLogic' in aug + case AugmentationType.MEMORY: + return 'storeData' in aug && 'retrieveData' in aug && 'updateData' in aug + case AugmentationType.PERCEPTION: + return 'interpret' in aug && 'organize' in aug && 'generateVisualization' in aug + case AugmentationType.DIALOG: + return 'processUserInput' in aug && 'generateResponse' in aug && 'manageContext' in aug + case AugmentationType.ACTIVATION: + return 'triggerAction' in aug && 'generateOutput' in aug && 'interactExternal' in aug + case AugmentationType.WEBSOCKET: + return 'connectWebSocket' in aug && 'sendWebSocketMessage' in aug && 'onWebSocketMessage' in aug + default: + return false + } + }) +} diff --git a/src/augmentationRegistryLoader.ts b/src/augmentationRegistryLoader.ts new file mode 100644 index 00000000..21493126 --- /dev/null +++ b/src/augmentationRegistryLoader.ts @@ -0,0 +1,291 @@ +/** + * Augmentation Registry Loader + * + * This module provides functionality for loading augmentation registrations + * at build time. It's designed to be used with build tools like webpack or rollup + * to automatically discover and register augmentations. + */ + +import { IAugmentation } from './types/augmentations.js' +import { registerAugmentation } from './augmentationRegistry.js' + +/** + * Options for the augmentation registry loader + */ +export interface AugmentationRegistryLoaderOptions { + /** + * Whether to automatically initialize the augmentations after loading + * @default false + */ + autoInitialize?: boolean; + + /** + * Whether to log debug information during loading + * @default false + */ + debug?: boolean; +} + +/** + * Default options for the augmentation registry loader + */ +const DEFAULT_OPTIONS: AugmentationRegistryLoaderOptions = { + autoInitialize: false, + debug: false +} + +/** + * Result of loading augmentations + */ +export interface AugmentationLoadResult { + /** + * The augmentations that were loaded + */ + augmentations: IAugmentation[]; + + /** + * Any errors that occurred during loading + */ + errors: Error[]; +} + +/** + * Loads augmentations from the specified modules + * + * This function is designed to be used with build tools like webpack or rollup + * to automatically discover and register augmentations. + * + * @param modules An object containing modules with augmentations to register + * @param options Options for the loader + * @returns A promise that resolves with the result of loading the augmentations + * + * @example + * ```typescript + * // webpack.config.js + * const { AugmentationRegistryPlugin } = require('brainy/dist/webpack'); + * + * module.exports = { + * // ... other webpack config + * plugins: [ + * new AugmentationRegistryPlugin({ + * // Pattern to match files containing augmentations + * pattern: /augmentation\.js$/, + * // Options for the loader + * options: { + * autoInitialize: true, + * debug: true + * } + * }) + * ] + * }; + * ``` + */ +export async function loadAugmentationsFromModules( + modules: Record, + options: AugmentationRegistryLoaderOptions = {} +): Promise { + const opts = { ...DEFAULT_OPTIONS, ...options } + const result: AugmentationLoadResult = { + augmentations: [], + errors: [] + } + + if (opts.debug) { + console.log(`[AugmentationRegistryLoader] Loading augmentations from ${Object.keys(modules).length} modules`) + } + + // Process each module + for (const [modulePath, module] of Object.entries(modules)) { + try { + if (opts.debug) { + console.log(`[AugmentationRegistryLoader] Processing module: ${modulePath}`) + } + + // Extract augmentations from the module + const augmentations = extractAugmentationsFromModule(module) + + if (augmentations.length === 0) { + if (opts.debug) { + console.log(`[AugmentationRegistryLoader] No augmentations found in module: ${modulePath}`) + } + continue + } + + // Register each augmentation + for (const augmentation of augmentations) { + try { + const registered = registerAugmentation(augmentation) + result.augmentations.push(registered) + + if (opts.debug) { + console.log(`[AugmentationRegistryLoader] Registered augmentation: ${registered.name}`) + } + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)) + result.errors.push(err) + + if (opts.debug) { + console.error(`[AugmentationRegistryLoader] Failed to register augmentation: ${err.message}`) + } + } + } + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)) + result.errors.push(err) + + if (opts.debug) { + console.error(`[AugmentationRegistryLoader] Error processing module ${modulePath}: ${err.message}`) + } + } + } + + if (opts.debug) { + console.log(`[AugmentationRegistryLoader] Loaded ${result.augmentations.length} augmentations with ${result.errors.length} errors`) + } + + return result +} + +/** + * Extracts augmentations from a module + * + * @param module The module to extract augmentations from + * @returns An array of augmentations found in the module + */ +function extractAugmentationsFromModule(module: any): IAugmentation[] { + const augmentations: IAugmentation[] = [] + + // If the module itself is an augmentation, add it + if (isAugmentation(module)) { + augmentations.push(module) + } + + // Check for exported augmentations + if (module && typeof module === 'object') { + for (const key of Object.keys(module)) { + const exported = module[key] + + // Skip non-objects and null + if (!exported || typeof exported !== 'object') { + continue + } + + // If the exported value is an augmentation, add it + if (isAugmentation(exported)) { + augmentations.push(exported) + } + + // If the exported value is an array of augmentations, add them + if (Array.isArray(exported) && exported.every(isAugmentation)) { + augmentations.push(...exported) + } + } + } + + return augmentations +} + +/** + * Checks if an object is an augmentation + * + * @param obj The object to check + * @returns True if the object is an augmentation + */ +function isAugmentation(obj: any): obj is IAugmentation { + return ( + obj && + typeof obj === 'object' && + typeof obj.name === 'string' && + typeof obj.initialize === 'function' && + typeof obj.shutDown === 'function' && + typeof obj.getStatus === 'function' + ) +} + +/** + * Creates a webpack plugin for automatically loading augmentations + * + * @param options Options for the plugin + * @returns A webpack plugin + * + * @example + * ```typescript + * // webpack.config.js + * const { createAugmentationRegistryPlugin } = require('brainy/dist/webpack'); + * + * module.exports = { + * // ... other webpack config + * plugins: [ + * createAugmentationRegistryPlugin({ + * pattern: /augmentation\.js$/, + * options: { + * autoInitialize: true, + * debug: true + * } + * }) + * ] + * }; + * ``` + */ +export function createAugmentationRegistryPlugin(options: { + /** + * Pattern to match files containing augmentations + */ + pattern: RegExp; + + /** + * Options for the loader + */ + options?: AugmentationRegistryLoaderOptions; +}) { + // This is just a placeholder - the actual implementation would depend on the build tool + return { + name: 'AugmentationRegistryPlugin', + pattern: options.pattern, + options: options.options || {} + } +} + +/** + * Creates a rollup plugin for automatically loading augmentations + * + * @param options Options for the plugin + * @returns A rollup plugin + * + * @example + * ```typescript + * // rollup.config.js + * import { createAugmentationRegistryRollupPlugin } from 'brainy/dist/rollup'; + * + * export default { + * // ... other rollup config + * plugins: [ + * createAugmentationRegistryRollupPlugin({ + * pattern: /augmentation\.js$/, + * options: { + * autoInitialize: true, + * debug: true + * } + * }) + * ] + * }; + * ``` + */ +export function createAugmentationRegistryRollupPlugin(options: { + /** + * Pattern to match files containing augmentations + */ + pattern: RegExp; + + /** + * Options for the loader + */ + options?: AugmentationRegistryLoaderOptions; +}) { + // This is just a placeholder - the actual implementation would depend on the build tool + return { + name: 'augmentation-registry-rollup-plugin', + pattern: options.pattern, + options: options.options || {} + } +} diff --git a/src/augmentations/README.md b/src/augmentations/README.md new file mode 100644 index 00000000..8eb6a3c9 --- /dev/null +++ b/src/augmentations/README.md @@ -0,0 +1,251 @@ +
+Brainy Logo + +# Brainy Augmentations + +
+ +This directory contains the augmentation implementations for Brainy. Augmentations are pluggable components that extend +Brainy's functionality in various ways. + +## Available Augmentations + +### Conduit Augmentations + +Conduit augmentations provide data synchronization between Brainy instances. + +#### WebSocketConduitAugmentation + +A conduit augmentation that syncs Brainy instances using WebSockets. This is used for syncing between browsers and +servers, or between servers. + +```javascript +import { createConduitAugmentation, augmentationPipeline } from '@soulcraft/brainy' + +// Create a WebSocket conduit augmentation +const wsConduit = await createConduitAugmentation('websocket', 'my-websocket-sync') + +// Register the augmentation with the pipeline +augmentationPipeline.register(wsConduit) + +// Connect to another Brainy instance +const connectionResult = await wsConduit.establishConnection( + 'wss://your-websocket-server.com/brainy-sync', + { protocols: 'brainy-sync' } +) +``` + +#### WebRTCConduitAugmentation + +A conduit augmentation that syncs Brainy instances using WebRTC. This is used for direct peer-to-peer syncing between +browsers. + +```javascript +import { createConduitAugmentation, augmentationPipeline } from '@soulcraft/brainy' + +// Create a WebRTC conduit augmentation +const webrtcConduit = await createConduitAugmentation('webrtc', 'my-webrtc-sync') + +// Register the augmentation with the pipeline +augmentationPipeline.register(webrtcConduit) + +// Connect to a peer +const connectionResult = await webrtcConduit.establishConnection( + 'peer-id-to-connect-to', + { + signalServerUrl: 'wss://your-signal-server.com', + localPeerId: 'my-peer-id', + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + } +) +``` + +#### ServerSearchConduitAugmentation + +A specialized conduit augmentation that provides functionality for searching a server-hosted Brainy instance and storing +results locally. This allows you to: + +- Search a server-hosted Brainy instance from a browser +- Store the search results in a local Brainy instance +- Perform further searches against the local instance without needing to query the server again +- Add data to both local and server instances + +```javascript +import { + ServerSearchConduitAugmentation, + createServerSearchAugmentations, + augmentationPipeline +} from '@soulcraft/brainy' + +// Using the factory function (recommended) +const { conduit, activation, connection } = await createServerSearchAugmentations( + 'wss://your-brainy-server.com/ws', + { protocols: 'brainy-sync' } +) + +// Register the augmentations with the pipeline +augmentationPipeline.register(conduit) +augmentationPipeline.register(activation) + +// Search the server and store results locally +const serverSearchResult = await conduit.searchServer( + connection.connectionId, + 'your search query', + 5 // limit +) + +// Search the local instance +const localSearchResult = await conduit.searchLocal('your search query', 5) + +// Perform a combined search (local first, then server if needed) +const combinedSearchResult = await conduit.searchCombined( + connection.connectionId, + 'your search query', + 5 +) + +// Add data to both local and server +const addResult = await conduit.addToBoth( + connection.connectionId, + 'Text to add', + { /* metadata */ } +) +``` + +### Activation Augmentations + +Activation augmentations dictate how Brainy initiates actions, responses, or data manipulations. + +#### ServerSearchActivationAugmentation + +An activation augmentation that provides actions for server search functionality. This works in conjunction with the +ServerSearchConduitAugmentation to provide a complete solution for browser-server search. + +```javascript +import { + ServerSearchActivationAugmentation, + createServerSearchAugmentations, + augmentationPipeline +} from '@soulcraft/brainy' + +// Using the factory function (recommended) +const { conduit, activation, connection } = await createServerSearchAugmentations( + 'wss://your-brainy-server.com/ws', + { protocols: 'brainy-sync' } +) + +// Register the augmentations with the pipeline +augmentationPipeline.register(conduit) +augmentationPipeline.register(activation) + +// Use the activation augmentation to search the server +const serverSearchAction = activation.triggerAction('searchServer', { + connectionId: connection.connectionId, + query: 'your search query', + limit: 5 +}) + +if (serverSearchAction.success) { + // The data property contains a promise that will resolve to the search results + const serverSearchResult = await serverSearchAction.data + console.log('Server search results:', serverSearchResult) +} + +// Other available actions: +// - 'connectToServer': Connect to a server +// - 'searchLocal': Search the local instance +// - 'searchCombined': Search both local and server +// - 'addToBoth': Add data to both local and server +``` + +## Using the Augmentation Pipeline + +The augmentation pipeline provides a way to execute augmentations based on their type. + +```javascript +import { augmentationPipeline } from '@soulcraft/brainy' + +// Execute a conduit augmentation +const conduitResults = await augmentationPipeline.executeConduitPipeline( + 'methodName', + [arg1, arg2, ...], + { /* options */ } +) + +// Execute an activation augmentation +const activationResults = await augmentationPipeline.executeActivationPipeline( + 'methodName', + [arg1, arg2, ...], + { /* options */ } +) +``` + +## Creating Custom Augmentations + +To create a custom augmentation, implement one of the augmentation interfaces: + +- `ISenseAugmentation`: For processing raw data +- `IConduitAugmentation`: For data synchronization +- `ICognitionAugmentation`: For reasoning and inference +- `IMemoryAugmentation`: For data storage +- `IPerceptionAugmentation`: For data interpretation and visualization +- `IDialogAugmentation`: For natural language processing +- `IActivationAugmentation`: For triggering actions + +Example: + +```javascript +import { AugmentationType, IActivationAugmentation } from '@soulcraft/brainy' + +class MyCustomActivation implements IActivationAugmentation { + readonly + name = 'my-custom-activation' + readonly + description = 'My custom activation augmentation' + enabled = true + + getType(): AugmentationType { + return AugmentationType.ACTIVATION + } + + async initialize(): Promise { + // Initialization code + } + + async shutDown(): Promise { + // Cleanup code + } + + async getStatus(): Promise<'active' | 'inactive' | 'error'> { + return 'active' + } + + triggerAction(actionName: string, parameters + +?: + + Record + +): + + AugmentationResponse { + // Implementation + } + + generateOutput(knowledgeId: string, format: string): AugmentationResponse> { + // Implementation +} + +interactExternal(systemId +: +string, payload +: +Record < string, unknown > +): +AugmentationResponse < unknown > { + // Implementation +} +} +``` diff --git a/src/augmentations/conduitAugmentations.ts b/src/augmentations/conduitAugmentations.ts new file mode 100644 index 00000000..ba94e647 --- /dev/null +++ b/src/augmentations/conduitAugmentations.ts @@ -0,0 +1,1409 @@ +import { + AugmentationType, + IConduitAugmentation, + IWebSocketSupport, + AugmentationResponse, + WebSocketConnection +} from '../types/augmentations.js' +import { v4 as uuidv4 } from 'uuid' + +/** + * Base class for conduit augmentations that provide data synchronization between Brainy instances + */ +abstract class BaseConduitAugmentation implements IConduitAugmentation { + readonly name: string + readonly description: string = 'Base conduit augmentation' + enabled: boolean = true + protected isInitialized = false + protected connections: Map = new Map() + + constructor(name: string) { + this.name = name + } + + async initialize(): Promise { + if (this.isInitialized) { + return + } + + try { + this.isInitialized = true + } catch (error) { + console.error(`Failed to initialize ${this.name}:`, error) + throw new Error(`Failed to initialize ${this.name}: ${error}`) + } + } + + async shutDown(): Promise { + // Close all connections + for (const [connectionId, connection] of this.connections.entries()) { + try { + if (connection.close) { + await connection.close() + } + } catch (error) { + console.error(`Failed to close connection ${connectionId}:`, error) + } + } + + this.connections.clear() + this.isInitialized = false + } + + async getStatus(): Promise<'active' | 'inactive' | 'error'> { + return this.isInitialized ? 'active' : 'inactive' + } + + abstract establishConnection( + targetSystemId: string, + config: Record + ): Promise> + + abstract readData( + query: Record, + options?: Record + ): Promise> + + abstract writeData( + data: Record, + options?: Record + ): Promise> + + abstract monitorStream( + streamId: string, + callback: (data: unknown) => void + ): Promise + + protected async ensureInitialized(): Promise { + if (!this.isInitialized) { + await this.initialize() + } + } +} + +/** + * WebSocket conduit augmentation for syncing Brainy instances using WebSockets + * + * This conduit is for syncing between browsers and servers, or between servers. + * WebSockets cannot be used for direct browser-to-browser communication without a server in the middle. + */ +export class WebSocketConduitAugmentation extends BaseConduitAugmentation implements IWebSocketSupport { + readonly description = 'Conduit augmentation that syncs Brainy instances using WebSockets' + private webSocketConnections: Map = new Map() + private messageCallbacks: Map void>> = new Map() + + constructor(name: string = 'websocket-conduit') { + super(name) + } + + getType(): AugmentationType { + return AugmentationType.CONDUIT + } + + /** + * Establishes a connection to another Brainy instance + * @param targetSystemId The URL or identifier of the target system + * @param config Configuration options for the connection + */ + async establishConnection( + targetSystemId: string, + config: Record + ): Promise> { + await this.ensureInitialized() + + try { + // For WebSocket connections, targetSystemId should be a WebSocket URL + const url = targetSystemId + const protocols = config.protocols as string | string[] | undefined + + // Create a WebSocket connection + const connection = await this.connectWebSocket(url, protocols) + + // Store the connection + this.connections.set(connection.connectionId, connection) + + return { + success: true, + data: connection + } + } catch (error) { + console.error(`Failed to establish connection to ${targetSystemId}:`, error) + return { + success: false, + data: null as any, + error: `Failed to establish connection: ${error}` + } + } + } + + /** + * Reads data from a connected Brainy instance + * @param query Query parameters for reading data + * @param options Additional options + */ + async readData( + query: Record, + options?: Record + ): Promise> { + await this.ensureInitialized() + + try { + const connectionId = query.connectionId as string + + if (!connectionId) { + throw new Error('connectionId is required for reading data') + } + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`Connection ${connectionId} not found`) + } + + // Create a request message + const requestMessage = { + type: 'read', + query: query.query || {}, + requestId: uuidv4(), + options + } + + // Send the request + await this.sendWebSocketMessage(connectionId, requestMessage) + + // Return a promise that will be resolved when the response is received + return new Promise((resolve) => { + const responseHandler = (data: unknown) => { + // Check if this is the response to our request + const response = data as any + if (response && response.type === 'readResponse' && response.requestId === requestMessage.requestId) { + // Remove the handler + this.offWebSocketMessage(connectionId, responseHandler) + + // Resolve with the response data + resolve({ + success: response.success, + data: response.data, + error: response.error + }) + } + } + + // Register the response handler + this.onWebSocketMessage(connectionId, responseHandler) + + // Set a timeout to prevent hanging + setTimeout(() => { + this.offWebSocketMessage(connectionId, responseHandler) + resolve({ + success: false, + data: null, + error: 'Timeout waiting for read response' + }) + }, 30000) // 30 second timeout + }) + } catch (error) { + console.error(`Failed to read data:`, error) + return { + success: false, + data: null, + error: `Failed to read data: ${error}` + } + } + } + + /** + * Writes data to a connected Brainy instance + * @param data The data to write + * @param options Additional options + */ + async writeData( + data: Record, + options?: Record + ): Promise> { + await this.ensureInitialized() + + try { + const connectionId = data.connectionId as string + + if (!connectionId) { + throw new Error('connectionId is required for writing data') + } + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`Connection ${connectionId} not found`) + } + + // Create a write message + const writeMessage = { + type: 'write', + data: data.data || {}, + requestId: uuidv4(), + options + } + + // Send the write message + await this.sendWebSocketMessage(connectionId, writeMessage) + + // Return a promise that will be resolved when the response is received + return new Promise((resolve) => { + const responseHandler = (data: unknown) => { + // Check if this is the response to our request + const response = data as any + if (response && response.type === 'writeResponse' && response.requestId === writeMessage.requestId) { + // Remove the handler + this.offWebSocketMessage(connectionId, responseHandler) + + // Resolve with the response data + resolve({ + success: response.success, + data: response.data, + error: response.error + }) + } + } + + // Register the response handler + this.onWebSocketMessage(connectionId, responseHandler) + + // Set a timeout to prevent hanging + setTimeout(() => { + this.offWebSocketMessage(connectionId, responseHandler) + resolve({ + success: false, + data: null, + error: 'Timeout waiting for write response' + }) + }, 30000) // 30 second timeout + }) + } catch (error) { + console.error(`Failed to write data:`, error) + return { + success: false, + data: null, + error: `Failed to write data: ${error}` + } + } + } + + /** + * Monitors a data stream from a connected Brainy instance + * @param streamId The ID of the stream to monitor (usually a connection ID) + * @param callback Function to call when new data is received + */ + async monitorStream( + streamId: string, + callback: (data: unknown) => void + ): Promise { + await this.ensureInitialized() + + try { + const connection = this.webSocketConnections.get(streamId) + + if (!connection) { + throw new Error(`Connection ${streamId} not found`) + } + + // Register the callback for all messages on this connection + await this.onWebSocketMessage(streamId, callback) + + } catch (error) { + console.error(`Failed to monitor stream ${streamId}:`, error) + throw new Error(`Failed to monitor stream: ${error}`) + } + } + + /** + * Establishes a WebSocket connection + * @param url The WebSocket server URL to connect to + * @param protocols Optional subprotocols + */ + async connectWebSocket( + url: string, + protocols?: string | string[] + ): Promise { + await this.ensureInitialized() + + return new Promise((resolve, reject) => { + try { + // Check if WebSocket is available + if (typeof WebSocket === 'undefined') { + throw new Error('WebSocket is not available in this environment') + } + + // Create a new WebSocket connection + const ws = new WebSocket(url, protocols) + const connectionId = uuidv4() + + // Create a connection object + const connection: WebSocketConnection = { + connectionId, + url, + status: 'disconnected', + send: async (data) => { + if (ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket is not open') + } + ws.send(data) + }, + close: async () => { + ws.close() + } + } + + // Set up event handlers + ws.onopen = () => { + connection.status = 'connected' + resolve(connection) + } + + ws.onerror = (error) => { + connection.status = 'error' + console.error(`WebSocket error for ${url}:`, error) + if (ws.readyState !== WebSocket.OPEN) { + reject(new Error(`WebSocket connection failed: ${error}`)) + } + } + + ws.onclose = () => { + connection.status = 'disconnected' + // Remove from connections map + this.webSocketConnections.delete(connectionId) + // Remove all callbacks + this.messageCallbacks.delete(connectionId) + } + + // Create a message handler wrapper that will call all registered callbacks + const messageHandlerWrapper = (data: unknown) => { + const callbacks = this.messageCallbacks.get(connectionId) + if (callbacks) { + for (const callback of callbacks) { + try { + callback(data) + } catch (error) { + console.error(`Error in WebSocket message callback:`, error) + } + } + } + } + + // Store the message handler wrapper + connection._messageHandlerWrapper = messageHandlerWrapper + + // Set up the message handler + ws.onmessage = (event) => { + try { + // Parse the message if it's a string + let data = event.data + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch { + // If parsing fails, use the raw string + } + } + + // Call the message handler wrapper + messageHandlerWrapper(data) + } catch (error) { + console.error(`Error handling WebSocket message:`, error) + } + } + + // Store the stream message handler + connection._streamMessageHandler = (event) => ws.onmessage && ws.onmessage(event as any) + + // Store the connection + this.webSocketConnections.set(connectionId, connection) + + // Initialize the callbacks set + this.messageCallbacks.set(connectionId, new Set()) + + } catch (error) { + reject(error) + } + }) + } + + /** + * Sends data through an established WebSocket connection + * @param connectionId The identifier of the established connection + * @param data The data to send (will be serialized if not a string) + */ + async sendWebSocketMessage( + connectionId: string, + data: unknown + ): Promise { + await this.ensureInitialized() + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`WebSocket connection ${connectionId} not found`) + } + + if (!connection.send) { + throw new Error(`WebSocket connection ${connectionId} does not support sending messages`) + } + + // Serialize the data if it's not already a string or binary + let serializedData: string | ArrayBufferLike | Blob | ArrayBufferView + + if (typeof data === 'string' || + data instanceof ArrayBuffer || + data instanceof Blob || + ArrayBuffer.isView(data)) { + serializedData = data as any + } else { + // Convert to JSON string + serializedData = JSON.stringify(data) + } + + // Send the data + await connection.send(serializedData) + } + + /** + * Registers a callback for incoming WebSocket messages + * @param connectionId The identifier of the established connection + * @param callback The function to call when a message is received + */ + async onWebSocketMessage( + connectionId: string, + callback: (data: unknown) => void + ): Promise { + await this.ensureInitialized() + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`WebSocket connection ${connectionId} not found`) + } + + // Get or create the callbacks set for this connection + let callbacks = this.messageCallbacks.get(connectionId) + + if (!callbacks) { + callbacks = new Set() + this.messageCallbacks.set(connectionId, callbacks) + } + + // Add the callback + callbacks.add(callback) + } + + /** + * Removes a callback for incoming WebSocket messages + * @param connectionId The identifier of the established connection + * @param callback The function to remove from the callbacks + */ + async offWebSocketMessage( + connectionId: string, + callback: (data: unknown) => void + ): Promise { + await this.ensureInitialized() + + const callbacks = this.messageCallbacks.get(connectionId) + + if (callbacks) { + callbacks.delete(callback) + } + } + + /** + * Closes an established WebSocket connection + * @param connectionId The identifier of the established connection + * @param code Optional close code + * @param reason Optional close reason + */ + async closeWebSocket( + connectionId: string, + code?: number, + reason?: string + ): Promise { + await this.ensureInitialized() + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`WebSocket connection ${connectionId} not found`) + } + + if (!connection.close) { + throw new Error(`WebSocket connection ${connectionId} does not support closing`) + } + + // Close the connection + await connection.close() + + // Remove from connections map + this.webSocketConnections.delete(connectionId) + + // Remove all callbacks + this.messageCallbacks.delete(connectionId) + } +} + +/** + * WebRTC conduit augmentation for syncing Brainy instances using WebRTC + * + * This conduit is for direct peer-to-peer syncing between browsers. + * It is the recommended approach for browser-to-browser communication. + */ +export class WebRTCConduitAugmentation extends BaseConduitAugmentation implements IWebSocketSupport { + readonly description = 'Conduit augmentation that syncs Brainy instances using WebRTC' + private peerConnections: Map = new Map() + private dataChannels: Map = new Map() + private webSocketConnections: Map = new Map() + private messageCallbacks: Map void>> = new Map() + private signalServer: WebSocketConnection | null = null + + constructor(name: string = 'webrtc-conduit') { + super(name) + } + + getType(): AugmentationType { + return AugmentationType.CONDUIT + } + + async initialize(): Promise { + if (this.isInitialized) { + return + } + + try { + // Check if WebRTC is available + if (typeof RTCPeerConnection === 'undefined') { + throw new Error('WebRTC is not available in this environment') + } + + this.isInitialized = true + } catch (error) { + console.error(`Failed to initialize ${this.name}:`, error) + throw new Error(`Failed to initialize ${this.name}: ${error}`) + } + } + + /** + * Establishes a connection to another Brainy instance using WebRTC + * @param targetSystemId The peer ID or signal server URL + * @param config Configuration options for the connection + */ + async establishConnection( + targetSystemId: string, + config: Record + ): Promise> { + await this.ensureInitialized() + + try { + // For WebRTC, we need to: + // 1. Connect to a signaling server (if not already connected) + // 2. Create a peer connection + // 3. Create a data channel + // 4. Exchange ICE candidates and SDP offers/answers + + // Check if we need to connect to a signaling server + if (!this.signalServer && config.signalServerUrl) { + // Connect to the signaling server + this.signalServer = await this.connectWebSocket(config.signalServerUrl as string) + + // Set up message handling for the signaling server + await this.onWebSocketMessage(this.signalServer.connectionId, async (data) => { + // Handle signaling messages + const message = data as any + + if (message.type === 'ice-candidate' && message.targetPeerId === config.localPeerId) { + // Add ICE candidate to the appropriate peer connection + const peerConnection = this.peerConnections.get(message.sourcePeerId) + if (peerConnection) { + try { + await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate)) + } catch (error) { + console.error(`Failed to add ICE candidate:`, error) + } + } + } else if (message.type === 'offer' && message.targetPeerId === config.localPeerId) { + // Handle incoming offer + await this.handleOffer(message.sourcePeerId, message.offer, config) + } else if (message.type === 'answer' && message.targetPeerId === config.localPeerId) { + // Handle incoming answer + const peerConnection = this.peerConnections.get(message.sourcePeerId) + if (peerConnection) { + try { + await peerConnection.setRemoteDescription(new RTCSessionDescription(message.answer)) + } catch (error) { + console.error(`Failed to set remote description:`, error) + } + } + } + }) + } + + // Create a peer connection + const peerConnection = new RTCPeerConnection({ + iceServers: (config.iceServers as RTCIceServer[]) || [ + { urls: 'stun:stun.l.google.com:19302' } + ] + }) + + // Generate a connection ID + const connectionId = uuidv4() + + // Store the peer connection + this.peerConnections.set(targetSystemId, peerConnection) + + // Create a data channel + const dataChannel = peerConnection.createDataChannel('brainy-sync', { + ordered: true + }) + + // Set up data channel event handlers + dataChannel.onopen = () => { + console.log(`Data channel to ${targetSystemId} opened`) + } + + dataChannel.onclose = () => { + console.log(`Data channel to ${targetSystemId} closed`) + // Clean up + this.dataChannels.delete(targetSystemId) + this.peerConnections.delete(targetSystemId) + this.webSocketConnections.delete(connectionId) + this.messageCallbacks.delete(connectionId) + } + + dataChannel.onerror = (error) => { + console.error(`Data channel error:`, error) + } + + // Create a message handler wrapper that will call all registered callbacks + const messageHandlerWrapper = (data: unknown) => { + const callbacks = this.messageCallbacks.get(connectionId) + if (callbacks) { + for (const callback of callbacks) { + try { + callback(data) + } catch (error) { + console.error(`Error in WebRTC message callback:`, error) + } + } + } + } + + dataChannel.onmessage = (event) => { + try { + // Parse the message if it's a string + let data = event.data + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch { + // If parsing fails, use the raw string + } + } + + // Call the message handler wrapper + messageHandlerWrapper(data) + } catch (error) { + console.error(`Error handling WebRTC message:`, error) + } + } + + // Store the data channel + this.dataChannels.set(targetSystemId, dataChannel) + + // Set up ICE candidate handling + peerConnection.onicecandidate = (event) => { + if (event.candidate && this.signalServer) { + // Send the ICE candidate to the peer via the signaling server + this.sendWebSocketMessage(this.signalServer.connectionId, { + type: 'ice-candidate', + sourcePeerId: config.localPeerId, + targetPeerId: targetSystemId, + candidate: event.candidate + }) + } + } + + // Create a WebSocket-like connection object for the WebRTC connection + const connection: WebSocketConnection = { + connectionId, + url: `webrtc://${targetSystemId}`, + status: 'disconnected', + send: async (data) => { + const dc = this.dataChannels.get(targetSystemId) + if (!dc || dc.readyState !== 'open') { + throw new Error('WebRTC data channel is not open') + } + + // Send the data + if (typeof data === 'string') { + dc.send(data) + } else if (data instanceof Blob) { + dc.send(data) + } else if (data instanceof ArrayBuffer) { + dc.send(new Uint8Array(data)) + } else if (ArrayBuffer.isView(data)) { + dc.send(data) + } else { + // Convert to JSON string + dc.send(JSON.stringify(data)) + } + }, + close: async () => { + const dc = this.dataChannels.get(targetSystemId) + if (dc) { + dc.close() + } + + const pc = this.peerConnections.get(targetSystemId) + if (pc) { + pc.close() + } + + // Clean up + this.dataChannels.delete(targetSystemId) + this.peerConnections.delete(targetSystemId) + this.webSocketConnections.delete(connectionId) + this.messageCallbacks.delete(connectionId) + }, + _messageHandlerWrapper: messageHandlerWrapper + } + + // Store the connection + this.webSocketConnections.set(connectionId, connection) + + // Initialize the callbacks set + this.messageCallbacks.set(connectionId, new Set()) + + // Create and send an offer + const offer = await peerConnection.createOffer() + await peerConnection.setLocalDescription(offer) + + // Send the offer to the peer via the signaling server + if (this.signalServer) { + await this.sendWebSocketMessage(this.signalServer.connectionId, { + type: 'offer', + sourcePeerId: config.localPeerId, + targetPeerId: targetSystemId, + offer + }) + } + + // Return the connection + return { + success: true, + data: connection + } + } catch (error) { + console.error(`Failed to establish WebRTC connection to ${targetSystemId}:`, error) + return { + success: false, + data: null as any, + error: `Failed to establish WebRTC connection: ${error}` + } + } + } + + /** + * Handles an incoming WebRTC offer + * @param peerId The ID of the peer sending the offer + * @param offer The SDP offer + * @param config Configuration options + */ + private async handleOffer( + peerId: string, + offer: RTCSessionDescriptionInit, + config: Record + ): Promise { + try { + // Create a peer connection if it doesn't exist + let peerConnection = this.peerConnections.get(peerId) + + if (!peerConnection) { + peerConnection = new RTCPeerConnection({ + iceServers: (config.iceServers as RTCIceServer[]) || [ + { urls: 'stun:stun.l.google.com:19302' } + ] + }) + + // Store the peer connection + this.peerConnections.set(peerId, peerConnection) + + // Set up ICE candidate handling + peerConnection.onicecandidate = (event) => { + if (event.candidate && this.signalServer) { + // Send the ICE candidate to the peer via the signaling server + this.sendWebSocketMessage(this.signalServer.connectionId, { + type: 'ice-candidate', + sourcePeerId: config.localPeerId, + targetPeerId: peerId, + candidate: event.candidate + }) + } + } + + // Handle data channel creation by the remote peer + peerConnection.ondatachannel = (event) => { + const dataChannel = event.channel + + // Generate a connection ID + const connectionId = uuidv4() + + // Store the data channel + this.dataChannels.set(peerId, dataChannel) + + // Set up data channel event handlers + dataChannel.onopen = () => { + console.log(`Data channel from ${peerId} opened`) + } + + dataChannel.onclose = () => { + console.log(`Data channel from ${peerId} closed`) + // Clean up + this.dataChannels.delete(peerId) + this.peerConnections.delete(peerId) + this.webSocketConnections.delete(connectionId) + this.messageCallbacks.delete(connectionId) + } + + dataChannel.onerror = (error) => { + console.error(`Data channel error:`, error) + } + + // Create a message handler wrapper that will call all registered callbacks + const messageHandlerWrapper = (data: unknown) => { + const callbacks = this.messageCallbacks.get(connectionId) + if (callbacks) { + for (const callback of callbacks) { + try { + callback(data) + } catch (error) { + console.error(`Error in WebRTC message callback:`, error) + } + } + } + } + + dataChannel.onmessage = (event) => { + try { + // Parse the message if it's a string + let data = event.data + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch { + // If parsing fails, use the raw string + } + } + + // Call the message handler wrapper + messageHandlerWrapper(data) + } catch (error) { + console.error(`Error handling WebRTC message:`, error) + } + } + + // Create a WebSocket-like connection object for the WebRTC connection + const connection: WebSocketConnection = { + connectionId, + url: `webrtc://${peerId}`, + status: 'disconnected', + send: async (data) => { + if (dataChannel.readyState !== 'open') { + throw new Error('WebRTC data channel is not open') + } + + // Send the data + if (typeof data === 'string') { + dataChannel.send(data) + } else if (data instanceof Blob) { + dataChannel.send(data) + } else if (data instanceof ArrayBuffer) { + dataChannel.send(new Uint8Array(data)) + } else if (ArrayBuffer.isView(data)) { + dataChannel.send(data) + } else { + // Convert to JSON string + dataChannel.send(JSON.stringify(data)) + } + }, + close: async () => { + dataChannel.close() + + const pc = this.peerConnections.get(peerId) + if (pc) { + pc.close() + } + + // Clean up + this.dataChannels.delete(peerId) + this.peerConnections.delete(peerId) + this.webSocketConnections.delete(connectionId) + this.messageCallbacks.delete(connectionId) + }, + _messageHandlerWrapper: messageHandlerWrapper + } + + // Store the connection + this.webSocketConnections.set(connectionId, connection) + + // Initialize the callbacks set + this.messageCallbacks.set(connectionId, new Set()) + } + } + + // Set the remote description (the offer) + await peerConnection.setRemoteDescription(new RTCSessionDescription(offer)) + + // Create an answer + const answer = await peerConnection.createAnswer() + await peerConnection.setLocalDescription(answer) + + // Send the answer to the peer via the signaling server + if (this.signalServer) { + await this.sendWebSocketMessage(this.signalServer.connectionId, { + type: 'answer', + sourcePeerId: config.localPeerId, + targetPeerId: peerId, + answer + }) + } + } catch (error) { + console.error(`Failed to handle WebRTC offer:`, error) + throw new Error(`Failed to handle WebRTC offer: ${error}`) + } + } + + /** + * Reads data from a connected Brainy instance + * @param query Query parameters for reading data + * @param options Additional options + */ + async readData( + query: Record, + options?: Record + ): Promise> { + await this.ensureInitialized() + + try { + const connectionId = query.connectionId as string + + if (!connectionId) { + throw new Error('connectionId is required for reading data') + } + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`Connection ${connectionId} not found`) + } + + // Create a request message + const requestMessage = { + type: 'read', + query: query.query || {}, + requestId: uuidv4(), + options + } + + // Send the request + await this.sendWebSocketMessage(connectionId, requestMessage) + + // Return a promise that will be resolved when the response is received + return new Promise((resolve) => { + const responseHandler = (data: unknown) => { + // Check if this is the response to our request + const response = data as any + if (response && response.type === 'readResponse' && response.requestId === requestMessage.requestId) { + // Remove the handler + this.offWebSocketMessage(connectionId, responseHandler) + + // Resolve with the response data + resolve({ + success: response.success, + data: response.data, + error: response.error + }) + } + } + + // Register the response handler + this.onWebSocketMessage(connectionId, responseHandler) + + // Set a timeout to prevent hanging + setTimeout(() => { + this.offWebSocketMessage(connectionId, responseHandler) + resolve({ + success: false, + data: null, + error: 'Timeout waiting for read response' + }) + }, 30000) // 30 second timeout + }) + } catch (error) { + console.error(`Failed to read data:`, error) + return { + success: false, + data: null, + error: `Failed to read data: ${error}` + } + } + } + + /** + * Writes data to a connected Brainy instance + * @param data The data to write + * @param options Additional options + */ + async writeData( + data: Record, + options?: Record + ): Promise> { + await this.ensureInitialized() + + try { + const connectionId = data.connectionId as string + + if (!connectionId) { + throw new Error('connectionId is required for writing data') + } + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`Connection ${connectionId} not found`) + } + + // Create a write message + const writeMessage = { + type: 'write', + data: data.data || {}, + requestId: uuidv4(), + options + } + + // Send the write message + await this.sendWebSocketMessage(connectionId, writeMessage) + + // Return a promise that will be resolved when the response is received + return new Promise((resolve) => { + const responseHandler = (data: unknown) => { + // Check if this is the response to our request + const response = data as any + if (response && response.type === 'writeResponse' && response.requestId === writeMessage.requestId) { + // Remove the handler + this.offWebSocketMessage(connectionId, responseHandler) + + // Resolve with the response data + resolve({ + success: response.success, + data: response.data, + error: response.error + }) + } + } + + // Register the response handler + this.onWebSocketMessage(connectionId, responseHandler) + + // Set a timeout to prevent hanging + setTimeout(() => { + this.offWebSocketMessage(connectionId, responseHandler) + resolve({ + success: false, + data: null, + error: 'Timeout waiting for write response' + }) + }, 30000) // 30 second timeout + }) + } catch (error) { + console.error(`Failed to write data:`, error) + return { + success: false, + data: null, + error: `Failed to write data: ${error}` + } + } + } + + /** + * Monitors a data stream from a connected Brainy instance + * @param streamId The ID of the stream to monitor (usually a connection ID) + * @param callback Function to call when new data is received + */ + async monitorStream( + streamId: string, + callback: (data: unknown) => void + ): Promise { + await this.ensureInitialized() + + try { + const connection = this.webSocketConnections.get(streamId) + + if (!connection) { + throw new Error(`Connection ${streamId} not found`) + } + + // Register the callback for all messages on this connection + await this.onWebSocketMessage(streamId, callback) + + } catch (error) { + console.error(`Failed to monitor stream ${streamId}:`, error) + throw new Error(`Failed to monitor stream: ${error}`) + } + } + + /** + * Establishes a WebSocket connection (used for signaling in WebRTC) + * @param url The WebSocket server URL to connect to + * @param protocols Optional subprotocols + */ + async connectWebSocket( + url: string, + protocols?: string | string[] + ): Promise { + await this.ensureInitialized() + + return new Promise((resolve, reject) => { + try { + // Check if WebSocket is available + if (typeof WebSocket === 'undefined') { + throw new Error('WebSocket is not available in this environment') + } + + // Create a new WebSocket connection + const ws = new WebSocket(url, protocols) + const connectionId = uuidv4() + + // Create a connection object + const connection: WebSocketConnection = { + connectionId, + url, + status: 'disconnected', + send: async (data) => { + if (ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket is not open') + } + ws.send(data) + }, + close: async () => { + ws.close() + } + } + + // Set up event handlers + ws.onopen = () => { + connection.status = 'connected' + resolve(connection) + } + + ws.onerror = (error) => { + connection.status = 'error' + console.error(`WebSocket error for ${url}:`, error) + if (ws.readyState !== WebSocket.OPEN) { + reject(new Error(`WebSocket connection failed: ${error}`)) + } + } + + ws.onclose = () => { + connection.status = 'disconnected' + // Remove from connections map + this.webSocketConnections.delete(connectionId) + // Remove all callbacks + this.messageCallbacks.delete(connectionId) + } + + // Create a message handler wrapper that will call all registered callbacks + const messageHandlerWrapper = (data: unknown) => { + const callbacks = this.messageCallbacks.get(connectionId) + if (callbacks) { + for (const callback of callbacks) { + try { + callback(data) + } catch (error) { + console.error(`Error in WebSocket message callback:`, error) + } + } + } + } + + // Store the message handler wrapper + connection._messageHandlerWrapper = messageHandlerWrapper + + // Set up the message handler + ws.onmessage = (event) => { + try { + // Parse the message if it's a string + let data = event.data + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch { + // If parsing fails, use the raw string + } + } + + // Call the message handler wrapper + messageHandlerWrapper(data) + } catch (error) { + console.error(`Error handling WebSocket message:`, error) + } + } + + // Store the stream message handler + connection._streamMessageHandler = (event) => ws.onmessage && ws.onmessage(event as any) + + // Store the connection + this.webSocketConnections.set(connectionId, connection) + + // Initialize the callbacks set + this.messageCallbacks.set(connectionId, new Set()) + + } catch (error) { + reject(error) + } + }) + } + + /** + * Sends data through an established WebSocket or WebRTC connection + * @param connectionId The identifier of the established connection + * @param data The data to send (will be serialized if not a string) + */ + async sendWebSocketMessage( + connectionId: string, + data: unknown + ): Promise { + await this.ensureInitialized() + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`Connection ${connectionId} not found`) + } + + if (!connection.send) { + throw new Error(`Connection ${connectionId} does not support sending messages`) + } + + // Serialize the data if it's not already a string or binary + let serializedData: string | ArrayBufferLike | Blob | ArrayBufferView + + if (typeof data === 'string' || + data instanceof ArrayBuffer || + data instanceof Blob || + ArrayBuffer.isView(data)) { + serializedData = data as any + } else { + // Convert to JSON string + serializedData = JSON.stringify(data) + } + + // Send the data + await connection.send(serializedData) + } + + /** + * Registers a callback for incoming WebSocket or WebRTC messages + * @param connectionId The identifier of the established connection + * @param callback The function to call when a message is received + */ + async onWebSocketMessage( + connectionId: string, + callback: (data: unknown) => void + ): Promise { + await this.ensureInitialized() + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`Connection ${connectionId} not found`) + } + + // Get or create the callbacks set for this connection + let callbacks = this.messageCallbacks.get(connectionId) + + if (!callbacks) { + callbacks = new Set() + this.messageCallbacks.set(connectionId, callbacks) + } + + // Add the callback + callbacks.add(callback) + } + + /** + * Removes a callback for incoming WebSocket or WebRTC messages + * @param connectionId The identifier of the established connection + * @param callback The function to remove from the callbacks + */ + async offWebSocketMessage( + connectionId: string, + callback: (data: unknown) => void + ): Promise { + await this.ensureInitialized() + + const callbacks = this.messageCallbacks.get(connectionId) + + if (callbacks) { + callbacks.delete(callback) + } + } + + /** + * Closes an established WebSocket or WebRTC connection + * @param connectionId The identifier of the established connection + * @param code Optional close code + * @param reason Optional close reason + */ + async closeWebSocket( + connectionId: string, + code?: number, + reason?: string + ): Promise { + await this.ensureInitialized() + + const connection = this.webSocketConnections.get(connectionId) + + if (!connection) { + throw new Error(`Connection ${connectionId} not found`) + } + + if (!connection.close) { + throw new Error(`Connection ${connectionId} does not support closing`) + } + + // Close the connection + await connection.close() + + // Remove from connections map + this.webSocketConnections.delete(connectionId) + + // Remove all callbacks + this.messageCallbacks.delete(connectionId) + } +} + +/** + * Factory function to create the appropriate conduit augmentation based on the type + */ +export async function createConduitAugmentation( + type: 'websocket' | 'webrtc', + name?: string, + options: Record = {} +): Promise { + switch (type) { + case 'websocket': + const wsAugmentation = new WebSocketConduitAugmentation(name || 'websocket-conduit') + await wsAugmentation.initialize() + return wsAugmentation + case 'webrtc': + const webrtcAugmentation = new WebRTCConduitAugmentation(name || 'webrtc-conduit') + await webrtcAugmentation.initialize() + return webrtcAugmentation + default: + throw new Error(`Unknown conduit augmentation type: ${type}`) + } +} diff --git a/src/augmentations/memoryAugmentations.ts b/src/augmentations/memoryAugmentations.ts new file mode 100644 index 00000000..a23f7c6e --- /dev/null +++ b/src/augmentations/memoryAugmentations.ts @@ -0,0 +1,333 @@ +import { + AugmentationType, + IMemoryAugmentation, + AugmentationResponse +} from '../types/augmentations.js' +import { StorageAdapter, Vector } from '../coreTypes.js' +import { MemoryStorage } from '../storage/opfsStorage.js' +import { FileSystemStorage } from '../storage/fileSystemStorage.js' +import { OPFSStorage } from '../storage/opfsStorage.js' +import { cosineDistance } from '../utils/distance.js' + +/** + * Base class for memory augmentations that wrap a StorageAdapter + */ +abstract class BaseMemoryAugmentation implements IMemoryAugmentation { + readonly name: string + readonly description: string = 'Base memory augmentation' + enabled: boolean = true + protected storage: StorageAdapter + protected isInitialized = false + + constructor(name: string, storage: StorageAdapter) { + this.name = name + this.storage = storage + } + + async initialize(): Promise { + if (this.isInitialized) { + return + } + + try { + await this.storage.init() + this.isInitialized = true + } catch (error) { + console.error(`Failed to initialize ${this.name}:`, error) + throw new Error(`Failed to initialize ${this.name}: ${error}`) + } + } + + async shutDown(): Promise { + this.isInitialized = false + } + + async getStatus(): Promise<'active' | 'inactive' | 'error'> { + return this.isInitialized ? 'active' : 'inactive' + } + + async storeData( + key: string, + data: unknown, + options?: Record + ): Promise> { + await this.ensureInitialized() + + try { + await this.storage.saveMetadata(key, data) + return { success: true, data: true } + } catch (error) { + console.error(`Failed to store data for key ${key}:`, error) + return { + success: false, + data: false, + error: `Failed to store data: ${error}` + } + } + } + + async retrieveData( + key: string, + options?: Record + ): Promise> { + await this.ensureInitialized() + + try { + const data = await this.storage.getMetadata(key) + return { + success: true, + data + } + } catch (error) { + console.error(`Failed to retrieve data for key ${key}:`, error) + return { + success: false, + data: null, + error: `Failed to retrieve data: ${error}` + } + } + } + + async updateData( + key: string, + data: unknown, + options?: Record + ): Promise> { + await this.ensureInitialized() + + try { + await this.storage.saveMetadata(key, data) + return { success: true, data: true } + } catch (error) { + console.error(`Failed to update data for key ${key}:`, error) + return { + success: false, + data: false, + error: `Failed to update data: ${error}` + } + } + } + + async deleteData( + key: string, + options?: Record + ): Promise> { + await this.ensureInitialized() + + try { + // There's no direct deleteMetadata method, so we save null + await this.storage.saveMetadata(key, null) + return { success: true, data: true } + } catch (error) { + console.error(`Failed to delete data for key ${key}:`, error) + return { + success: false, + data: false, + error: `Failed to delete data: ${error}` + } + } + } + + async listDataKeys( + pattern?: string, + options?: Record + ): Promise> { + // This is a limitation of the current StorageAdapter interface + // It doesn't provide a way to list all metadata keys + // We could implement this in the future by extending the StorageAdapter interface + return { + success: false, + data: [], + error: 'listDataKeys is not supported by this storage adapter' + } + } + + /** + * Searches for data in the storage using vector similarity. + * Implements the findNearest functionality by calculating distances client-side. + * @param query The query vector or data to search for + * @param k Number of results to return (default: 10) + * @param options Optional search options + */ + async search( + query: unknown, + k: number = 10, + options?: Record + ): Promise>> { + await this.ensureInitialized() + + try { + // Check if query is a vector + let queryVector: Vector + + if (Array.isArray(query) && query.every(item => typeof item === 'number')) { + queryVector = query as Vector + } else { + // If query is not a vector, we can't perform vector search + return { + success: false, + data: [], + error: 'Query must be a vector (array of numbers) for vector search' + } + } + + // Get all nodes from storage + const nodes = await this.storage.getAllNouns() + + // Calculate distances and prepare results + const results: Array<{ + id: string; + score: number; + data: unknown; + }> = [] + + for (const node of nodes) { + // Skip nodes that don't have a vector + if (!node.vector || !Array.isArray(node.vector)) { + continue + } + + // Get metadata for the node + const metadata = await this.storage.getMetadata(node.id) + + // Calculate distance between query vector and node vector + const distance = cosineDistance(queryVector, node.vector) + + // Convert distance to similarity score (1 - distance for cosine) + // This way higher scores are better (more similar) + const score = 1 - distance + + results.push({ + id: node.id, + score, + data: metadata + }) + } + + // Sort results by score (descending) and take top k + results.sort((a, b) => b.score - a.score) + const topResults = results.slice(0, k) + + return { + success: true, + data: topResults + } + } catch (error) { + console.error(`Failed to search in storage:`, error) + return { + success: false, + data: [], + error: `Failed to search in storage: ${error}` + } + } + } + + protected async ensureInitialized(): Promise { + if (!this.isInitialized) { + await this.initialize() + } + } +} + +/** + * Memory augmentation that uses in-memory storage + */ +export class MemoryStorageAugmentation extends BaseMemoryAugmentation { + readonly description = 'Memory augmentation that stores data in memory' + enabled = true + + constructor(name: string) { + super(name, new MemoryStorage()) + } + + getType(): AugmentationType { + return AugmentationType.MEMORY + } +} + +/** + * Memory augmentation that uses file system storage + */ +export class FileSystemStorageAugmentation extends BaseMemoryAugmentation { + readonly description = 'Memory augmentation that stores data in the file system' + enabled = true + + constructor(name: string, rootDirectory?: string) { + super(name, new FileSystemStorage(rootDirectory)) + } + + getType(): AugmentationType { + return AugmentationType.MEMORY + } +} + +/** + * Memory augmentation that uses OPFS (Origin Private File System) storage + */ +export class OPFSStorageAugmentation extends BaseMemoryAugmentation { + readonly description = 'Memory augmentation that stores data in the Origin Private File System' + enabled = true + + constructor(name: string) { + super(name, new OPFSStorage()) + } + + getType(): AugmentationType { + return AugmentationType.MEMORY + } +} + +/** + * Factory function to create the appropriate memory augmentation based on the environment + */ +export async function createMemoryAugmentation( + name: string, + options: { + storageType?: 'memory' | 'filesystem' | 'opfs' + rootDirectory?: string + requestPersistentStorage?: boolean + } = {} +): Promise { + // If a specific storage type is requested, use that + if (options.storageType) { + switch (options.storageType) { + case 'memory': + return new MemoryStorageAugmentation(name) + case 'filesystem': + return new FileSystemStorageAugmentation(name, options.rootDirectory) + case 'opfs': + return new OPFSStorageAugmentation(name) + } + } + + // Otherwise, select based on environment + // Use the global isNode variable from the environment detection + const isNodeEnv = globalThis.__ENV__?.isNode || ( + typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null + ) + + if (isNodeEnv) { + // In Node.js, use FileSystemStorage + return new FileSystemStorageAugmentation(name, options.rootDirectory) + } else { + // In browser, try OPFS first + const opfsStorage = new OPFSStorage() + + if (opfsStorage.isOPFSAvailable()) { + // Request persistent storage if specified + if (options.requestPersistentStorage) { + await opfsStorage.requestPersistentStorage() + } + return new OPFSStorageAugmentation(name) + } else { + // Fall back to memory storage + return new MemoryStorageAugmentation(name) + } + } +} diff --git a/src/augmentations/serverSearchAugmentations.ts b/src/augmentations/serverSearchAugmentations.ts new file mode 100644 index 00000000..93d593e3 --- /dev/null +++ b/src/augmentations/serverSearchAugmentations.ts @@ -0,0 +1,665 @@ +/** + * Server Search Augmentations + * + * This file implements conduit and activation augmentations for browser-server search functionality. + * It allows Brainy to search a server-hosted instance and store results locally. + */ + +import { + AugmentationType, + IConduitAugmentation, + IActivationAugmentation, + IWebSocketSupport, + AugmentationResponse, + WebSocketConnection +} from '../types/augmentations.js' +import { WebSocketConduitAugmentation } from './conduitAugmentations.js' +import { v4 as uuidv4 } from 'uuid' +import { BrainyDataInterface } from '../types/brainyDataInterface.js' + +/** + * ServerSearchConduitAugmentation + * + * A specialized conduit augmentation that provides functionality for searching + * a server-hosted Brainy instance and storing results locally. + */ +export class ServerSearchConduitAugmentation extends WebSocketConduitAugmentation { + private localDb: BrainyDataInterface | null = null + + constructor(name: string = 'server-search-conduit') { + super(name) + // this.description = 'Conduit augmentation for server-hosted Brainy search' + } + + /** + * Initialize the augmentation + */ + async initialize(): Promise { + if (this.isInitialized) { + return + } + + try { + // Initialize the base conduit + await super.initialize() + + // Local DB must be set before initialization + if (!this.localDb) { + throw new Error('Local database not set. Call setLocalDb before initializing.') + } + + this.isInitialized = true + } catch (error) { + console.error(`Failed to initialize ${this.name}:`, error) + throw new Error(`Failed to initialize ${this.name}: ${error}`) + } + } + + /** + * Set the local Brainy instance + * @param db The Brainy instance to use for local storage + */ + setLocalDb(db: BrainyDataInterface): void { + this.localDb = db + } + + /** + * Get the local Brainy instance + * @returns The local Brainy instance + */ + getLocalDb(): BrainyDataInterface | null { + return this.localDb + } + + /** + * Search the server-hosted Brainy instance and store results locally + * @param connectionId The ID of the established connection + * @param query The search query + * @param limit Maximum number of results to return + * @returns Search results + */ + async searchServer( + connectionId: string, + query: string, + limit: number = 10 + ): Promise> { + await this.ensureInitialized() + + try { + // Create a search request + const readResult = await this.readData({ + connectionId, + query: { + type: 'search', + query, + limit + } + }) + + if (readResult.success && readResult.data) { + const searchResults = readResult.data as any[] + + // Store the results in the local Brainy instance + if (this.localDb) { + for (const result of searchResults) { + // Check if the noun already exists in the local database + const existingNoun = await this.localDb.get(result.id) + + if (!existingNoun) { + // Add the noun to the local database + await this.localDb.add(result.vector, result.metadata) + } + } + } + + return { + success: true, + data: searchResults + } + } else { + return { + success: false, + data: null, + error: readResult.error || 'Unknown error searching server' + } + } + } catch (error) { + console.error('Error searching server:', error) + return { + success: false, + data: null, + error: `Error searching server: ${error}` + } + } + } + + /** + * Search the local Brainy instance + * @param query The search query + * @param limit Maximum number of results to return + * @returns Search results + */ + async searchLocal( + query: string, + limit: number = 10 + ): Promise> { + await this.ensureInitialized() + + try { + if (!this.localDb) { + return { + success: false, + data: null, + error: 'Local database not initialized' + } + } + + const results = await this.localDb.searchText(query, limit) + + return { + success: true, + data: results + } + } catch (error) { + console.error('Error searching local database:', error) + return { + success: false, + data: null, + error: `Error searching local database: ${error}` + } + } + } + + /** + * Search both server and local instances, combine results, and store server results locally + * @param connectionId The ID of the established connection + * @param query The search query + * @param limit Maximum number of results to return + * @returns Combined search results + */ + async searchCombined( + connectionId: string, + query: string, + limit: number = 10 + ): Promise> { + await this.ensureInitialized() + + try { + // Search local first + const localSearchResult = await this.searchLocal(query, limit) + + if (!localSearchResult.success) { + return localSearchResult + } + + const localResults = localSearchResult.data as any[] + + // If we have enough local results, return them + if (localResults.length >= limit) { + return localSearchResult + } + + // Otherwise, search server for additional results + const serverSearchResult = await this.searchServer( + connectionId, + query, + limit - localResults.length + ) + + if (!serverSearchResult.success) { + // If server search fails, return local results + return localSearchResult + } + + const serverResults = serverSearchResult.data as any[] + + // Combine results, removing duplicates + const combinedResults = [...localResults] + const localIds = new Set(localResults.map(r => r.id)) + + for (const result of serverResults) { + if (!localIds.has(result.id)) { + combinedResults.push(result) + } + } + + return { + success: true, + data: combinedResults + } + } catch (error) { + console.error('Error performing combined search:', error) + return { + success: false, + data: null, + error: `Error performing combined search: ${error}` + } + } + } + + /** + * Add data to both local and server instances + * @param connectionId The ID of the established connection + * @param data Text or vector to add + * @param metadata Metadata for the data + * @returns ID of the added data + */ + async addToBoth( + connectionId: string, + data: string | any[], + metadata: any = {} + ): Promise> { + await this.ensureInitialized() + + try { + if (!this.localDb) { + return { + success: false, + data: '', + error: 'Local database not initialized' + } + } + + // Add to local first + const id = await this.localDb.add(data, metadata) + + // Get the vector and metadata + const noun = await this.localDb.get(id) as import('../coreTypes.js').VectorDocument + + if (!noun) { + return { + success: false, + data: '', + error: 'Failed to retrieve newly created noun' + } + } + + // Add to server + const writeResult = await this.writeData({ + connectionId, + data: { + type: 'addNoun', + vector: noun.vector, + metadata: noun.metadata + } + }) + + if (!writeResult.success) { + return { + success: true, + data: id, + error: `Added locally but failed to add to server: ${writeResult.error}` + } + } + + return { + success: true, + data: id + } + } catch (error) { + console.error('Error adding data to both:', error) + return { + success: false, + data: '', + error: `Error adding data to both: ${error}` + } + } + } +} + +/** + * ServerSearchActivationAugmentation + * + * An activation augmentation that provides actions for server search functionality. + */ +export class ServerSearchActivationAugmentation implements IActivationAugmentation { + readonly name: string + readonly description: string + enabled: boolean = true + private isInitialized = false + private conduitAugmentation: ServerSearchConduitAugmentation | null = null + private connections: Map = new Map() + + constructor(name: string = 'server-search-activation') { + this.name = name + this.description = 'Activation augmentation for server-hosted Brainy search' + } + + getType(): AugmentationType { + return AugmentationType.ACTIVATION + } + + /** + * Initialize the augmentation + */ + async initialize(): Promise { + if (this.isInitialized) { + return + } + + this.isInitialized = true + } + + /** + * Shut down the augmentation + */ + async shutDown(): Promise { + this.isInitialized = false + } + + /** + * Get the status of the augmentation + */ + async getStatus(): Promise<'active' | 'inactive' | 'error'> { + return this.isInitialized ? 'active' : 'inactive' + } + + /** + * Set the conduit augmentation to use for server search + * @param conduit The ServerSearchConduitAugmentation to use + */ + setConduitAugmentation(conduit: ServerSearchConduitAugmentation): void { + this.conduitAugmentation = conduit + } + + /** + * Store a connection for later use + * @param connectionId The ID to use for the connection + * @param connection The WebSocket connection + */ + storeConnection(connectionId: string, connection: WebSocketConnection): void { + this.connections.set(connectionId, connection) + } + + /** + * Get a stored connection + * @param connectionId The ID of the connection to retrieve + * @returns The WebSocket connection + */ + getConnection(connectionId: string): WebSocketConnection | undefined { + return this.connections.get(connectionId) + } + + /** + * Trigger an action based on a processed command or internal state + * @param actionName The name of the action to trigger + * @param parameters Optional parameters for the action + */ + triggerAction( + actionName: string, + parameters?: Record + ): AugmentationResponse { + if (!this.conduitAugmentation) { + return { + success: false, + data: null, + error: 'Conduit augmentation not set' + } + } + + // Handle different actions + switch (actionName) { + case 'connectToServer': + return this.handleConnectToServer(parameters || {}) + case 'searchServer': + return this.handleSearchServer(parameters || {}) + case 'searchLocal': + return this.handleSearchLocal(parameters || {}) + case 'searchCombined': + return this.handleSearchCombined(parameters || {}) + case 'addToBoth': + return this.handleAddToBoth(parameters || {}) + default: + return { + success: false, + data: null, + error: `Unknown action: ${actionName}` + } + } + } + + /** + * Handle the connectToServer action + * @param parameters Action parameters + */ + private handleConnectToServer( + parameters: Record + ): AugmentationResponse { + const serverUrl = parameters.serverUrl as string + const protocols = parameters.protocols as string | string[] | undefined + + if (!serverUrl) { + return { + success: false, + data: null, + error: 'serverUrl parameter is required' + } + } + + // Return a promise that will be resolved when the connection is established + return { + success: true, + data: this.conduitAugmentation!.establishConnection(serverUrl, { + protocols + }) + } + } + + /** + * Handle the searchServer action + * @param parameters Action parameters + */ + private handleSearchServer( + parameters: Record + ): AugmentationResponse { + const connectionId = parameters.connectionId as string + const query = parameters.query as string + const limit = parameters.limit as number || 10 + + if (!connectionId) { + return { + success: false, + data: null, + error: 'connectionId parameter is required' + } + } + + if (!query) { + return { + success: false, + data: null, + error: 'query parameter is required' + } + } + + // Return a promise that will be resolved when the search is complete + return { + success: true, + data: this.conduitAugmentation!.searchServer(connectionId, query, limit) + } + } + + /** + * Handle the searchLocal action + * @param parameters Action parameters + */ + private handleSearchLocal( + parameters: Record + ): AugmentationResponse { + const query = parameters.query as string + const limit = parameters.limit as number || 10 + + if (!query) { + return { + success: false, + data: null, + error: 'query parameter is required' + } + } + + // Return a promise that will be resolved when the search is complete + return { + success: true, + data: this.conduitAugmentation!.searchLocal(query, limit) + } + } + + /** + * Handle the searchCombined action + * @param parameters Action parameters + */ + private handleSearchCombined( + parameters: Record + ): AugmentationResponse { + const connectionId = parameters.connectionId as string + const query = parameters.query as string + const limit = parameters.limit as number || 10 + + if (!connectionId) { + return { + success: false, + data: null, + error: 'connectionId parameter is required' + } + } + + if (!query) { + return { + success: false, + data: null, + error: 'query parameter is required' + } + } + + // Return a promise that will be resolved when the search is complete + return { + success: true, + data: this.conduitAugmentation!.searchCombined(connectionId, query, limit) + } + } + + /** + * Handle the addToBoth action + * @param parameters Action parameters + */ + private handleAddToBoth( + parameters: Record + ): AugmentationResponse { + const connectionId = parameters.connectionId as string + const data = parameters.data + const metadata = parameters.metadata || {} + + if (!connectionId) { + return { + success: false, + data: null, + error: 'connectionId parameter is required' + } + } + + if (!data) { + return { + success: false, + data: null, + error: 'data parameter is required' + } + } + + // Return a promise that will be resolved when the add is complete + return { + success: true, + data: this.conduitAugmentation!.addToBoth(connectionId, data as any, metadata as any) + } + } + + /** + * Generates an expressive output or response from Brainy + * @param knowledgeId The identifier of the knowledge to express + * @param format The desired output format (e.g., 'text', 'json') + */ + generateOutput( + knowledgeId: string, + format: string + ): AugmentationResponse> { + // This method is not used for server search functionality + return { + success: false, + data: '', + error: 'generateOutput is not implemented for ServerSearchActivationAugmentation' + } + } + + /** + * Interacts with an external system or API + * @param systemId The identifier of the external system + * @param payload The data to send to the external system + */ + interactExternal( + systemId: string, + payload: Record + ): AugmentationResponse { + // This method is not used for server search functionality + return { + success: false, + data: null, + error: 'interactExternal is not implemented for ServerSearchActivationAugmentation' + } + } +} + +/** + * Factory function to create server search augmentations + * @param serverUrl The URL of the server to connect to + * @param options Additional options + * @returns An object containing the created augmentations + */ +export async function createServerSearchAugmentations( + serverUrl: string, + options: { + conduitName?: string, + activationName?: string, + protocols?: string | string[], + localDb?: BrainyDataInterface + } = {} +): Promise<{ + conduit: ServerSearchConduitAugmentation, + activation: ServerSearchActivationAugmentation, + connection: WebSocketConnection +}> { + // Create the conduit augmentation + const conduit = new ServerSearchConduitAugmentation(options.conduitName) + await conduit.initialize() + + // Set the local database if provided + if (options.localDb) { + conduit.setLocalDb(options.localDb) + } + + // Create the activation augmentation + const activation = new ServerSearchActivationAugmentation(options.activationName) + await activation.initialize() + + // Link the augmentations + activation.setConduitAugmentation(conduit) + + // Connect to the server + const connectionResult = await conduit.establishConnection( + serverUrl, + { protocols: options.protocols } + ) + + if (!connectionResult.success || !connectionResult.data) { + throw new Error(`Failed to connect to server: ${connectionResult.error}`) + } + + const connection = connectionResult.data + + // Store the connection in the activation augmentation + activation.storeConnection(connection.connectionId, connection) + + return { + conduit, + activation, + connection + } +} diff --git a/src/brainyData.ts b/src/brainyData.ts new file mode 100644 index 00000000..348f1e78 --- /dev/null +++ b/src/brainyData.ts @@ -0,0 +1,2252 @@ +/** + * BrainyData + * Main class that provides the vector database functionality + */ + +import { v4 as uuidv4 } from 'uuid' +import { HNSWIndex } from './hnsw/hnswIndex.js' +import { HNSWIndexOptimized, HNSWOptimizedConfig } from './hnsw/hnswIndexOptimized.js' +import { createStorage } from './storage/opfsStorage.js' +import { + DistanceFunction, + GraphVerb, + EmbeddingFunction, + HNSWConfig, + HNSWNoun, + SearchResult, + StorageAdapter, + Vector, + VectorDocument +} from './coreTypes.js' +import { + cosineDistance, + defaultEmbeddingFunction, + euclideanDistance +} from './utils/index.js' +import { NounType, VerbType, GraphNoun } from './types/graphTypes.js' +import { + ServerSearchConduitAugmentation, + createServerSearchAugmentations +} from './augmentations/serverSearchAugmentations.js' +import { WebSocketConnection } from './types/augmentations.js' +import { BrainyDataInterface } from './types/brainyDataInterface.js' + +export interface BrainyDataConfig { + /** + * HNSW index configuration + */ + hnsw?: Partial + + /** + * Optimized HNSW index configuration + * If provided, will use the optimized HNSW index instead of the standard one + */ + hnswOptimized?: Partial + + /** + * Distance function to use for similarity calculations + */ + distanceFunction?: DistanceFunction + + /** + * Custom storage adapter (if not provided, will use OPFS or memory storage) + */ + storageAdapter?: StorageAdapter + + /** + * Storage configuration options + * These will be passed to createStorage if storageAdapter is not provided + */ + storage?: { + requestPersistentStorage?: boolean + r2Storage?: { + bucketName?: string + accountId?: string + accessKeyId?: string + secretAccessKey?: string + } + s3Storage?: { + bucketName?: string + accessKeyId?: string + secretAccessKey?: string + region?: string + } + gcsStorage?: { + bucketName?: string + accessKeyId?: string + secretAccessKey?: string + endpoint?: string + } + customS3Storage?: { + bucketName?: string + accessKeyId?: string + secretAccessKey?: string + endpoint?: string + region?: string + } + forceFileSystemStorage?: boolean + forceMemoryStorage?: boolean + } + + /** + * Embedding function to convert data to vectors + */ + embeddingFunction?: EmbeddingFunction + + + /** + * Set the database to read-only mode + * When true, all write operations will throw an error + */ + readOnly?: boolean + + /** + * Remote server configuration for search operations + */ + remoteServer?: { + /** + * WebSocket URL of the remote Brainy server + */ + url: string + + /** + * WebSocket protocols to use for the connection + */ + protocols?: string | string[] + + /** + * Whether to automatically connect to the remote server on initialization + */ + autoConnect?: boolean + } +} + +export class BrainyData implements BrainyDataInterface { + private index: HNSWIndex | HNSWIndexOptimized + private storage: StorageAdapter | null = null + private isInitialized = false + private embeddingFunction: EmbeddingFunction + private distanceFunction: DistanceFunction + private requestPersistentStorage: boolean + private readOnly: boolean + private storageConfig: BrainyDataConfig['storage'] = {} + private useOptimizedIndex: boolean = false + + // Remote server properties + private remoteServerConfig: BrainyDataConfig['remoteServer'] | null = null + private serverSearchConduit: ServerSearchConduitAugmentation | null = null + private serverConnection: WebSocketConnection | null = null + + /** + * Create a new vector database + */ + constructor(config: BrainyDataConfig = {}) { + // Set distance function + this.distanceFunction = config.distanceFunction || cosineDistance + + // Check if optimized HNSW index configuration is provided + if (config.hnswOptimized) { + // Initialize optimized HNSW index + this.index = new HNSWIndexOptimized( + config.hnswOptimized, + this.distanceFunction, + config.storageAdapter || null + ) + this.useOptimizedIndex = true + } else { + // Initialize standard HNSW index + this.index = new HNSWIndex(config.hnsw, this.distanceFunction) + } + + // Set storage if provided, otherwise it will be initialized in init() + this.storage = config.storageAdapter || null + + // Set embedding function if provided, otherwise use default + this.embeddingFunction = + config.embeddingFunction || defaultEmbeddingFunction + + // Set persistent storage request flag + this.requestPersistentStorage = config.storage?.requestPersistentStorage || false + + // Set read-only flag + this.readOnly = config.readOnly || false + + // Store storage configuration for later use in init() + this.storageConfig = config.storage || {} + + // Store remote server configuration if provided + if (config.remoteServer) { + this.remoteServerConfig = config.remoteServer + } + } + + /** + * Check if the database is in read-only mode and throw an error if it is + * @throws Error if the database is in read-only mode + */ + private checkReadOnly(): void { + if (this.readOnly) { + throw new Error( + 'Cannot perform write operation: database is in read-only mode' + ) + } + } + + /** + * Initialize the database + * Loads existing data from storage if available + */ + public async init(): Promise { + if (this.isInitialized) { + return + } + + try { + // Initialize storage if not provided in constructor + if (!this.storage) { + // Combine storage config with requestPersistentStorage for backward compatibility + const storageOptions = { + ...this.storageConfig, + requestPersistentStorage: this.requestPersistentStorage + } + + this.storage = await createStorage(storageOptions) + } + + // Initialize storage + await this.storage!.init() + + // If using optimized index, set the storage adapter + if (this.useOptimizedIndex && this.index instanceof HNSWIndexOptimized) { + this.index.setStorage(this.storage!) + } + + // Load all nouns from storage + const nouns: HNSWNoun[] = await this.storage!.getAllNouns() + + // Clear the index and add all nouns + this.index.clear() + for (const noun of nouns) { + // Add to index + await this.index.addItem({ + id: noun.id, + vector: noun.vector + }) + } + + // Connect to remote server if configured with autoConnect + if (this.remoteServerConfig && this.remoteServerConfig.autoConnect) { + try { + await this.connectToRemoteServer( + this.remoteServerConfig.url, + this.remoteServerConfig.protocols + ) + } catch (remoteError) { + console.warn('Failed to auto-connect to remote server:', remoteError) + // Continue initialization even if remote connection fails + } + } + + this.isInitialized = true + } catch (error) { + console.error('Failed to initialize BrainyData:', error) + throw new Error(`Failed to initialize BrainyData: ${error}`) + } + } + + /** + * Connect to a remote Brainy server for search operations + * @param serverUrl WebSocket URL of the remote Brainy server + * @param protocols Optional WebSocket protocols to use + * @returns The connection object + */ + public async connectToRemoteServer( + serverUrl: string, + protocols?: string | string[] + ): Promise { + await this.ensureInitialized() + + try { + // Create server search augmentations + const { conduit, connection } = await createServerSearchAugmentations( + serverUrl, + { + protocols, + localDb: this + } + ) + + // Store the conduit and connection + this.serverSearchConduit = conduit + this.serverConnection = connection + + return connection + } catch (error) { + console.error('Failed to connect to remote server:', error) + throw new Error(`Failed to connect to remote server: ${error}`) + } + } + + /** + * Add a vector or data to the database + * If the input is not a vector, it will be converted using the embedding function + * @param vectorOrData Vector or data to add + * @param metadata Optional metadata to associate with the vector + * @param options Additional options + * @returns The ID of the added vector + */ + public async add( + vectorOrData: Vector | any, + metadata?: T, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + addToRemote?: boolean // Whether to also add to the remote server if connected + id?: string // Optional ID to use instead of generating a new one + } = {} + ): Promise { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + try { + let vector: Vector + + // Check if input is already a vector + if ( + Array.isArray(vectorOrData) && + vectorOrData.every((item) => typeof item === 'number') && + !options.forceEmbed + ) { + // Input is already a vector + vector = vectorOrData + } else { + // Input needs to be vectorized + try { + vector = await this.embeddingFunction(vectorOrData) + } catch (embedError) { + throw new Error(`Failed to vectorize data: ${embedError}`) + } + } + + // Check if vector is defined + if (!vector) { + throw new Error('Vector is undefined or null') + } + + // Use ID from options if it exists, otherwise from metadata, otherwise generate a new UUID + const id = + options.id || + (metadata && typeof metadata === 'object' && 'id' in metadata + ? (metadata as any).id + : uuidv4()) + + // Add to index + await this.index.addItem({ id, vector }) + + // Get the noun from the index + const noun = this.index.getNouns().get(id) + + if (!noun) { + throw new Error(`Failed to retrieve newly created noun with ID ${id}`) + } + + // Save noun to storage + await this.storage!.saveNoun(noun) + + // Save metadata if provided + if (metadata !== undefined) { + // Validate noun type if metadata is for a GraphNoun + if (metadata && typeof metadata === 'object' && 'noun' in metadata) { + const nounType = (metadata as unknown as GraphNoun).noun + + // Check if the noun type is valid + const isValidNounType = Object.values(NounType).includes(nounType) + + if (!isValidNounType) { + console.warn( + `Invalid noun type: ${nounType}. Falling back to GraphNoun.` + ) + // Set a default noun type + ;(metadata as unknown as GraphNoun).noun = NounType.Concept + } + } + + await this.storage!.saveMetadata(id, metadata) + } + + // If addToRemote is true and we're connected to a remote server, add to remote as well + if (options.addToRemote && this.isConnectedToRemoteServer()) { + try { + await this.addToRemote(id, vector, metadata) + } catch (remoteError) { + console.warn( + `Failed to add to remote server: ${remoteError}. Continuing with local add.` + ) + } + } + + return id + } catch (error) { + console.error('Failed to add vector:', error) + throw new Error(`Failed to add vector: ${error}`) + } + } + + /** + * Add data to both local and remote Brainy instances + * @param vectorOrData Vector or data to add + * @param metadata Optional metadata to associate with the vector + * @param options Additional options + * @returns The ID of the added vector + */ + public async addToBoth( + vectorOrData: Vector | any, + metadata?: T, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + } = {} + ): Promise { + // Check if connected to a remote server + if (!this.isConnectedToRemoteServer()) { + throw new Error( + 'Not connected to a remote server. Call connectToRemoteServer() first.' + ) + } + + // Add to local with addToRemote option + return this.add(vectorOrData, metadata, { ...options, addToRemote: true }) + } + + /** + * Add a vector to the remote server + * @param id ID of the vector to add + * @param vector Vector to add + * @param metadata Optional metadata to associate with the vector + * @returns True if successful, false otherwise + * @private + */ + private async addToRemote( + id: string, + vector: Vector, + metadata?: T + ): Promise { + if (!this.isConnectedToRemoteServer()) { + return false + } + + try { + if (!this.serverSearchConduit || !this.serverConnection) { + throw new Error( + 'Server search conduit or connection is not initialized' + ) + } + + // Add to remote server + const addResult = await this.serverSearchConduit.addToBoth( + this.serverConnection.connectionId, + vector, + metadata + ) + + if (!addResult.success) { + throw new Error(`Remote add failed: ${addResult.error}`) + } + + return true + } catch (error) { + console.error('Failed to add to remote server:', error) + throw new Error(`Failed to add to remote server: ${error}`) + } + } + + /** + * Add multiple vectors or data items to the database + * @param items Array of items to add + * @param options Additional options + * @returns Array of IDs for the added items + */ + public async addBatch( + items: Array<{ + vectorOrData: Vector | any + metadata?: T + }>, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + addToRemote?: boolean // Whether to also add to the remote server if connected + concurrency?: number // Maximum number of concurrent operations (default: 4) + } = {} + ): Promise { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + // Default concurrency to 4 if not specified + const concurrency = options.concurrency || 4 + + try { + // Process items in batches to control concurrency + const ids: string[] = [] + const itemsToProcess = [...items] // Create a copy to avoid modifying the original array + + while (itemsToProcess.length > 0) { + // Take up to 'concurrency' items to process in parallel + const batch = itemsToProcess.splice(0, concurrency) + + // Process this batch in parallel + const batchResults = await Promise.all( + batch.map(item => this.add(item.vectorOrData, item.metadata, options)) + ) + + // Add the results to our ids array + ids.push(...batchResults) + } + + return ids + } catch (error) { + console.error('Failed to add batch of items:', error) + throw new Error(`Failed to add batch of items: ${error}`) + } + } + + /** + * Add multiple vectors or data items to both local and remote databases + * @param items Array of items to add + * @param options Additional options + * @returns Array of IDs for the added items + */ + public async addBatchToBoth( + items: Array<{ + vectorOrData: Vector | any + metadata?: T + }>, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + concurrency?: number // Maximum number of concurrent operations (default: 4) + } = {} + ): Promise { + // Check if connected to a remote server + if (!this.isConnectedToRemoteServer()) { + throw new Error( + 'Not connected to a remote server. Call connectToRemoteServer() first.' + ) + } + + // Add to local with addToRemote option + return this.addBatch(items, { ...options, addToRemote: true }) + } + + /** + * Search for similar vectors within specific noun types + * @param queryVectorOrData Query vector or data to search for + * @param k Number of results to return + * @param nounTypes Array of noun types to search within, or null to search all + * @param options Additional options + * @returns Array of search results + */ + public async searchByNounTypes( + queryVectorOrData: Vector | any, + k: number = 10, + nounTypes: string[] | null = null, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + } = {} + ): Promise[]> { + await this.ensureInitialized() + + try { + let queryVector: Vector + + // Check if input is already a vector + if ( + Array.isArray(queryVectorOrData) && + queryVectorOrData.every((item) => typeof item === 'number') && + !options.forceEmbed + ) { + // Input is already a vector + queryVector = queryVectorOrData + } else { + // Input needs to be vectorized + try { + queryVector = await this.embeddingFunction(queryVectorOrData) + } catch (embedError) { + throw new Error(`Failed to vectorize query data: ${embedError}`) + } + } + + // Check if query vector is defined + if (!queryVector) { + throw new Error('Query vector is undefined or null') + } + + // If no noun types specified, search all nouns + if (!nounTypes || nounTypes.length === 0) { + // Search in the index + const results = await this.index.search(queryVector, k) + + // Get metadata for each result + const searchResults: SearchResult[] = [] + + for (const [id, score] of results) { + const noun = this.index.getNouns().get(id) + if (!noun) { + continue + } + + const metadata = await this.storage!.getMetadata(id) + + searchResults.push({ + id, + score, + vector: noun.vector, + metadata: metadata as T | undefined + }) + } + + return searchResults + } else { + // Get nouns for each noun type in parallel + const nounPromises = nounTypes.map((nounType) => + this.storage!.getNounsByNounType(nounType) + ) + const nounArrays = await Promise.all(nounPromises) + + // Combine all nouns + const nouns: HNSWNoun[] = [] + for (const nounArray of nounArrays) { + nouns.push(...nounArray) + } + + // Calculate distances for each noun + const results: Array<[string, number]> = [] + for (const noun of nouns) { + const distance = this.index.getDistanceFunction()( + queryVector, + noun.vector + ) + results.push([noun.id, distance]) + } + + // Sort by distance (ascending) + results.sort((a, b) => a[1] - b[1]) + + // Take top k results + const topResults = results.slice(0, k) + + // Get metadata for each result + const searchResults: SearchResult[] = [] + + for (const [id, score] of topResults) { + const noun = nouns.find((n) => n.id === id) + if (!noun) { + continue + } + + const metadata = await this.storage!.getMetadata(id) + + searchResults.push({ + id, + score, + vector: noun.vector, + metadata: metadata as T | undefined + }) + } + + return searchResults + } + } catch (error) { + console.error('Failed to search vectors by noun types:', error) + throw new Error(`Failed to search vectors by noun types: ${error}`) + } + } + + /** + * Search for similar vectors + * @param queryVectorOrData Query vector or data to search for + * @param k Number of results to return + * @param options Additional options + * @returns Array of search results + */ + public async search( + queryVectorOrData: Vector | any, + k: number = 10, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + nounTypes?: string[] // Optional array of noun types to search within + includeVerbs?: boolean // Whether to include associated GraphVerbs in the results + searchMode?: 'local' | 'remote' | 'combined' // Where to search: local, remote, or both + searchVerbs?: boolean // Whether to search for verbs directly instead of nouns + verbTypes?: string[] // Optional array of verb types to search within or filter by + searchConnectedNouns?: boolean // Whether to search for nouns connected by verbs + verbDirection?: 'outgoing' | 'incoming' | 'both' // Direction of verbs to consider when searching connected nouns + } = {} + ): Promise[]> { + // If searching for verbs directly + if (options.searchVerbs) { + const verbResults = await this.searchVerbs(queryVectorOrData, k, { + forceEmbed: options.forceEmbed, + verbTypes: options.verbTypes + }); + + // Convert verb results to SearchResult format + return verbResults.map(verb => ({ + id: verb.id, + score: verb.similarity, + vector: verb.embedding || [], + metadata: { + verb: verb.verb, + source: verb.source, + target: verb.target, + ...verb.data + } as unknown as T + })); + } + + // If searching for nouns connected by verbs + if (options.searchConnectedNouns) { + return this.searchNounsByVerbs(queryVectorOrData, k, { + forceEmbed: options.forceEmbed, + verbTypes: options.verbTypes, + direction: options.verbDirection + }); + } + + // If a specific search mode is specified, use the appropriate search method + if (options.searchMode === 'local') { + return this.searchLocal(queryVectorOrData, k, options) + } else if (options.searchMode === 'remote') { + return this.searchRemote(queryVectorOrData, k, options) + } else if (options.searchMode === 'combined') { + return this.searchCombined(queryVectorOrData, k, options) + } + + // Default behavior (backward compatible): search locally + return this.searchLocal(queryVectorOrData, k, options) + } + + /** + * Search the local database for similar vectors + * @param queryVectorOrData Query vector or data to search for + * @param k Number of results to return + * @param options Additional options + * @returns Array of search results + */ + public async searchLocal( + queryVectorOrData: Vector | any, + k: number = 10, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + nounTypes?: string[] // Optional array of noun types to search within + includeVerbs?: boolean // Whether to include associated GraphVerbs in the results + } = {} + ): Promise[]> { + // If input is a string and not a vector, automatically vectorize it + let queryToUse = queryVectorOrData + if (typeof queryVectorOrData === 'string' && !options.forceEmbed) { + queryToUse = await this.embed(queryVectorOrData) + options.forceEmbed = false // Already embedded, don't force again + } + + // If noun types are specified, use searchByNounTypes + let searchResults + if (options.nounTypes && options.nounTypes.length > 0) { + searchResults = await this.searchByNounTypes( + queryToUse, + k, + options.nounTypes, + { + forceEmbed: options.forceEmbed + } + ) + } else { + // Otherwise, search all GraphNouns + searchResults = await this.searchByNounTypes(queryToUse, k, null, { + forceEmbed: options.forceEmbed + }) + } + + // If includeVerbs is true, retrieve associated GraphVerbs for each result + if (options.includeVerbs && this.storage) { + for (const result of searchResults) { + try { + // Get outgoing verbs for this noun + const outgoingVerbs = await this.storage.getVerbsBySource(result.id) + + // Get incoming verbs for this noun + const incomingVerbs = await this.storage.getVerbsByTarget(result.id) + + // Combine all verbs + const allVerbs = [...outgoingVerbs, ...incomingVerbs] + + // Add verbs to the result metadata + if (!result.metadata) { + result.metadata = {} as T + } + + // Add the verbs to the metadata + ;(result.metadata as Record).associatedVerbs = allVerbs + } catch (error) { + console.warn(`Failed to retrieve verbs for noun ${result.id}:`, error) + } + } + } + + return searchResults + } + + /** + * Find entities similar to a given entity ID + * @param id ID of the entity to find similar entities for + * @param options Additional options + * @returns Array of search results with similarity scores + */ + public async findSimilar( + id: string, + options: { + limit?: number // Number of results to return + nounTypes?: string[] // Optional array of noun types to search within + includeVerbs?: boolean // Whether to include associated GraphVerbs in the results + searchMode?: 'local' | 'remote' | 'combined' // Where to search: local, remote, or both + } = {} + ): Promise[]> { + await this.ensureInitialized() + + // Get the entity by ID + const entity = await this.get(id) + if (!entity) { + throw new Error(`Entity with ID ${id} not found`) + } + + // Use the entity's vector to search for similar entities + const k = (options.limit || 10) + 1 // Add 1 to account for the original entity + const searchResults = await this.search(entity.vector, k, { + forceEmbed: false, + nounTypes: options.nounTypes, + includeVerbs: options.includeVerbs, + searchMode: options.searchMode + }) + + // Filter out the original entity and limit to the requested number + return searchResults + .filter(result => result.id !== id) + .slice(0, options.limit || 10) + } + + /** + * Get a vector by ID + */ + public async get(id: string): Promise | null> { + await this.ensureInitialized() + + try { + // Get noun from index + const noun = this.index.getNouns().get(id) + if (!noun) { + return null + } + + // Get metadata + const metadata = await this.storage!.getMetadata(id) + + return { + id, + vector: noun.vector, + metadata: metadata as T | undefined + } + } catch (error) { + console.error(`Failed to get vector ${id}:`, error) + throw new Error(`Failed to get vector ${id}: ${error}`) + } + } + + /** + * Get all nouns in the database + * @returns Array of vector documents + */ + public async getAllNouns(): Promise[]> { + await this.ensureInitialized() + + try { + const nouns = this.index.getNouns() + const result: VectorDocument[] = [] + + for (const [id, noun] of nouns.entries()) { + const metadata = await this.storage!.getMetadata(id) + result.push({ + id, + vector: noun.vector, + metadata: metadata as T | undefined + }) + } + + return result + } catch (error) { + console.error('Failed to get all nouns:', error) + throw new Error(`Failed to get all nouns: ${error}`) + } + } + + /** + * Delete a vector by ID + */ + public async delete(id: string): Promise { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + try { + // Remove from index + const removed = this.index.removeItem(id) + if (!removed) { + return false + } + + // Remove from storage + await this.storage!.deleteNoun(id) + + // Try to remove metadata (ignore errors) + try { + await this.storage!.saveMetadata(id, null) + } catch (error) { + // Ignore + } + + return true + } catch (error) { + console.error(`Failed to delete vector ${id}:`, error) + throw new Error(`Failed to delete vector ${id}: ${error}`) + } + } + + /** + * Update metadata for a vector + */ + public async updateMetadata(id: string, metadata: T): Promise { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + try { + // Check if a vector exists + const noun = this.index.getNouns().get(id) + if (!noun) { + return false + } + + // Validate noun type if metadata is for a GraphNoun + if (metadata && typeof metadata === 'object' && 'noun' in metadata) { + const nounType = (metadata as unknown as GraphNoun).noun + + // Check if the noun type is valid + const isValidNounType = Object.values(NounType).includes(nounType) + + if (!isValidNounType) { + console.warn( + `Invalid noun type: ${nounType}. Falling back to GraphNoun.` + ) + // Set a default noun type + ;(metadata as unknown as GraphNoun).noun = NounType.Concept + } + } + + // Update metadata + await this.storage!.saveMetadata(id, metadata) + + return true + } catch (error) { + console.error(`Failed to update metadata for vector ${id}:`, error) + throw new Error(`Failed to update metadata for vector ${id}: ${error}`) + } + } + + /** + * Create a relationship between two entities + * This is a convenience wrapper around addVerb + */ + public async relate( + sourceId: string, + targetId: string, + relationType: string, + metadata?: any + ): Promise { + return this.addVerb(sourceId, targetId, undefined, { + type: relationType, + metadata: metadata + }) + } + + /** + * Add a verb between two nouns + * If metadata is provided and vector is not, the metadata will be vectorized using the embedding function + */ + public async addVerb( + sourceId: string, + targetId: string, + vector?: Vector, + options: { + type?: string + weight?: number + metadata?: any + forceEmbed?: boolean // Force using the embedding function for metadata even if vector is provided + id?: string // Optional ID to use instead of generating a new one + } = {} + ): Promise { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + try { + // Check if source and target nouns exist + const sourceNoun = this.index.getNouns().get(sourceId) + const targetNoun = this.index.getNouns().get(targetId) + + if (!sourceNoun) { + throw new Error(`Source noun with ID ${sourceId} not found`) + } + + if (!targetNoun) { + throw new Error(`Target noun with ID ${targetId} not found`) + } + + // Use provided ID or generate a new one + const id = options.id || uuidv4() + + let verbVector: Vector + + // If metadata is provided and no vector is provided or forceEmbed is true, vectorize the metadata + if (options.metadata && (!vector || options.forceEmbed)) { + try { + // Extract a string representation from metadata for embedding + let textToEmbed: string + if (typeof options.metadata === 'string') { + textToEmbed = options.metadata + } else if ( + options.metadata.description && + typeof options.metadata.description === 'string' + ) { + textToEmbed = options.metadata.description + } else { + // Convert to JSON string as fallback + textToEmbed = JSON.stringify(options.metadata) + } + + // Ensure textToEmbed is a string + if (typeof textToEmbed !== 'string') { + textToEmbed = String(textToEmbed) + } + + verbVector = await this.embeddingFunction(textToEmbed) + } catch (embedError) { + throw new Error(`Failed to vectorize verb metadata: ${embedError}`) + } + } else { + // Use a provided vector or average of source and target vectors + if (vector) { + verbVector = vector + } else { + // Ensure both source and target vectors have the same dimension + if ( + !sourceNoun.vector || + !targetNoun.vector || + sourceNoun.vector.length === 0 || + targetNoun.vector.length === 0 || + sourceNoun.vector.length !== targetNoun.vector.length + ) { + throw new Error( + `Cannot average vectors: source or target vector is invalid or dimensions don't match` + ) + } + + // Average the vectors + verbVector = sourceNoun.vector.map( + (val, i) => (val + targetNoun.vector[i]) / 2 + ) + } + } + + // Validate verb type if provided + let verbType = options.type + if (verbType) { + // Check if the verb type is valid + const isValidVerbType = Object.values(VerbType).includes( + verbType as VerbType + ) + + if (!isValidVerbType) { + console.warn( + `Invalid verb type: ${verbType}. Using RelatedTo as default.` + ) + // Set a default verb type + verbType = VerbType.RelatedTo + } + } + + // Create verb + const verb: GraphVerb = { + id, + vector: verbVector, + connections: new Map(), + sourceId, + targetId, + type: verbType, + weight: options.weight, + metadata: options.metadata + } + + // Add to index + await this.index.addItem({ id, vector: verbVector }) + + // Get the noun from the index + const indexNoun = this.index.getNouns().get(id) + + if (!indexNoun) { + throw new Error( + `Failed to retrieve newly created verb noun with ID ${id}` + ) + } + + // Update verb connections from index + verb.connections = indexNoun.connections + + // Save verb to storage + await this.storage!.saveVerb(verb) + + return id + } catch (error) { + console.error('Failed to add verb:', error) + throw new Error(`Failed to add verb: ${error}`) + } + } + + /** + * Get a verb by ID + */ + public async getVerb(id: string): Promise { + await this.ensureInitialized() + + try { + return await this.storage!.getVerb(id) + } catch (error) { + console.error(`Failed to get verb ${id}:`, error) + throw new Error(`Failed to get verb ${id}: ${error}`) + } + } + + /** + * Get all verbs + */ + public async getAllVerbs(): Promise { + await this.ensureInitialized() + + try { + return await this.storage!.getAllVerbs() + } catch (error) { + console.error('Failed to get all verbs:', error) + throw new Error(`Failed to get all verbs: ${error}`) + } + } + + /** + * Get verbs by source noun ID + */ + public async getVerbsBySource(sourceId: string): Promise { + await this.ensureInitialized() + + try { + return await this.storage!.getVerbsBySource(sourceId) + } catch (error) { + console.error(`Failed to get verbs by source ${sourceId}:`, error) + throw new Error(`Failed to get verbs by source ${sourceId}: ${error}`) + } + } + + /** + * Get verbs by target noun ID + */ + public async getVerbsByTarget(targetId: string): Promise { + await this.ensureInitialized() + + try { + return await this.storage!.getVerbsByTarget(targetId) + } catch (error) { + console.error(`Failed to get verbs by target ${targetId}:`, error) + throw new Error(`Failed to get verbs by target ${targetId}: ${error}`) + } + } + + /** + * Get verbs by type + */ + public async getVerbsByType(type: string): Promise { + await this.ensureInitialized() + + try { + return await this.storage!.getVerbsByType(type) + } catch (error) { + console.error(`Failed to get verbs by type ${type}:`, error) + throw new Error(`Failed to get verbs by type ${type}: ${error}`) + } + } + + /** + * Delete a verb + */ + public async deleteVerb(id: string): Promise { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + try { + // Remove from index + const removed = this.index.removeItem(id) + if (!removed) { + return false + } + + // Remove from storage + await this.storage!.deleteVerb(id) + + return true + } catch (error) { + console.error(`Failed to delete verb ${id}:`, error) + throw new Error(`Failed to delete verb ${id}: ${error}`) + } + } + + /** + * Clear the database + */ + public async clear(): Promise { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + try { + // Clear index + this.index.clear() + + // Clear storage + await this.storage!.clear() + } catch (error) { + console.error('Failed to clear vector database:', error) + throw new Error(`Failed to clear vector database: ${error}`) + } + } + + /** + * Get the number of vectors in the database + */ + public size(): number { + return this.index.size() + } + + /** + * Check if the database is in read-only mode + * @returns True if the database is in read-only mode, false otherwise + */ + public isReadOnly(): boolean { + return this.readOnly + } + + /** + * Set the database to read-only mode + * @param readOnly True to set the database to read-only mode, false to allow writes + */ + public setReadOnly(readOnly: boolean): void { + this.readOnly = readOnly + } + + /** + * Embed text or data into a vector using the same embedding function used by this instance + * This allows clients to use the same TensorFlow Universal Sentence Encoder throughout their application + * + * @param data Text or data to embed + * @returns A promise that resolves to the embedded vector + */ + public async embed(data: string | string[]): Promise { + await this.ensureInitialized() + + try { + return await this.embeddingFunction(data) + } catch (error) { + console.error('Failed to embed data:', error) + throw new Error(`Failed to embed data: ${error}`) + } + } + + /** + * Search for verbs by type and/or vector similarity + * @param queryVectorOrData Query vector or data to search for + * @param k Number of results to return + * @param options Additional options + * @returns Array of verbs with similarity scores + */ + public async searchVerbs( + queryVectorOrData: Vector | any, + k: number = 10, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + verbTypes?: string[] // Optional array of verb types to search within + } = {} + ): Promise> { + await this.ensureInitialized() + + try { + let queryVector: Vector + + // Check if input is already a vector + if ( + Array.isArray(queryVectorOrData) && + queryVectorOrData.every((item) => typeof item === 'number') && + !options.forceEmbed + ) { + // Input is already a vector + queryVector = queryVectorOrData + } else { + // Input needs to be vectorized + try { + queryVector = await this.embeddingFunction(queryVectorOrData) + } catch (embedError) { + throw new Error(`Failed to vectorize query data: ${embedError}`) + } + } + + // Get verbs to search through + let verbs: GraphVerb[] = [] + + // If verb types are specified, get verbs of those types + if (options.verbTypes && options.verbTypes.length > 0) { + // Get verbs for each verb type in parallel + const verbPromises = options.verbTypes.map((verbType) => + this.getVerbsByType(verbType) + ) + const verbArrays = await Promise.all(verbPromises) + + // Combine all verbs + for (const verbArray of verbArrays) { + verbs.push(...verbArray) + } + } else { + // Get all verbs + verbs = await this.storage!.getAllVerbs() + } + + // Filter out verbs without embeddings + verbs = verbs.filter(verb => verb.embedding && verb.embedding.length > 0) + + // Calculate similarity for each verb + const results: Array = [] + for (const verb of verbs) { + if (verb.embedding) { + const distance = this.index.getDistanceFunction()( + queryVector, + verb.embedding + ) + results.push({ + ...verb, + similarity: distance + }) + } + } + + // Sort by similarity (ascending distance) + results.sort((a, b) => a.similarity - b.similarity) + + // Take top k results + return results.slice(0, k) + } catch (error) { + console.error('Failed to search verbs:', error) + throw new Error(`Failed to search verbs: ${error}`) + } + } + + /** + * Search for nouns connected by specific verb types + * @param queryVectorOrData Query vector or data to search for + * @param k Number of results to return + * @param options Additional options + * @returns Array of search results + */ + public async searchNounsByVerbs( + queryVectorOrData: Vector | any, + k: number = 10, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + verbTypes?: string[] // Optional array of verb types to filter by + direction?: 'outgoing' | 'incoming' | 'both' // Direction of verbs to consider + } = {} + ): Promise[]> { + await this.ensureInitialized() + + try { + // First, search for nouns + const nounResults = await this.searchByNounTypes( + queryVectorOrData, + k * 2, // Get more results initially to account for filtering + null, + { forceEmbed: options.forceEmbed } + ) + + // If no verb types specified, return the noun results directly + if (!options.verbTypes || options.verbTypes.length === 0) { + return nounResults.slice(0, k) + } + + // For each noun, get connected nouns through specified verb types + const connectedNounIds = new Set() + const direction = options.direction || 'both' + + for (const result of nounResults) { + // Get verbs connected to this noun + let connectedVerbs: GraphVerb[] = [] + + if (direction === 'outgoing' || direction === 'both') { + // Get outgoing verbs + const outgoingVerbs = await this.storage!.getVerbsBySource(result.id) + connectedVerbs.push(...outgoingVerbs) + } + + if (direction === 'incoming' || direction === 'both') { + // Get incoming verbs + const incomingVerbs = await this.storage!.getVerbsByTarget(result.id) + connectedVerbs.push(...incomingVerbs) + } + + // Filter by verb types if specified + if (options.verbTypes && options.verbTypes.length > 0) { + connectedVerbs = connectedVerbs.filter(verb => + verb.verb && options.verbTypes!.includes(verb.verb) + ) + } + + // Add connected noun IDs to the set + for (const verb of connectedVerbs) { + if (verb.source && verb.source !== result.id) { + connectedNounIds.add(verb.source) + } + if (verb.target && verb.target !== result.id) { + connectedNounIds.add(verb.target) + } + } + } + + // Get the connected nouns + const connectedNouns: SearchResult[] = [] + for (const id of connectedNounIds) { + try { + const noun = this.index.getNouns().get(id) + if (noun) { + const metadata = await this.storage!.getMetadata(id) + + // Calculate similarity score + let queryVector: Vector + if ( + Array.isArray(queryVectorOrData) && + queryVectorOrData.every((item) => typeof item === 'number') && + !options.forceEmbed + ) { + queryVector = queryVectorOrData + } else { + queryVector = await this.embeddingFunction(queryVectorOrData) + } + + const distance = this.index.getDistanceFunction()( + queryVector, + noun.vector + ) + + connectedNouns.push({ + id, + score: distance, + vector: noun.vector, + metadata: metadata as T | undefined + }) + } + } catch (error) { + console.warn(`Failed to retrieve noun ${id}:`, error) + } + } + + // Sort by similarity score + connectedNouns.sort((a, b) => a.score - b.score) + + // Return top k results + return connectedNouns.slice(0, k) + } catch (error) { + console.error('Failed to search nouns by verbs:', error) + throw new Error(`Failed to search nouns by verbs: ${error}`) + } + } + + /** + * Search for similar documents using a text query + * This is a convenience method that embeds the query text and performs a search + * + * @param query Text query to search for + * @param k Number of results to return + * @param options Additional options + * @returns Array of search results + */ + public async searchText( + query: string, + k: number = 10, + options: { + nounTypes?: string[] + includeVerbs?: boolean + searchMode?: 'local' | 'remote' | 'combined' + } = {} + ): Promise[]> { + await this.ensureInitialized() + + try { + // Embed the query text + const queryVector = await this.embed(query) + + // Search using the embedded vector + return await this.search(queryVector, k, { + nounTypes: options.nounTypes, + includeVerbs: options.includeVerbs, + searchMode: options.searchMode + }) + } catch (error) { + console.error('Failed to search with text query:', error) + throw new Error(`Failed to search with text query: ${error}`) + } + } + + /** + * Search a remote Brainy server for similar vectors + * @param queryVectorOrData Query vector or data to search for + * @param k Number of results to return + * @param options Additional options + * @returns Array of search results + */ + public async searchRemote( + queryVectorOrData: Vector | any, + k: number = 10, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + nounTypes?: string[] // Optional array of noun types to search within + includeVerbs?: boolean // Whether to include associated GraphVerbs in the results + storeResults?: boolean // Whether to store the results in the local database (default: true) + } = {} + ): Promise[]> { + await this.ensureInitialized() + + // Check if connected to a remote server + if (!this.isConnectedToRemoteServer()) { + throw new Error( + 'Not connected to a remote server. Call connectToRemoteServer() first.' + ) + } + + try { + // If input is a string, convert it to a query string for the server + let query: string + if (typeof queryVectorOrData === 'string') { + query = queryVectorOrData + } else { + // For vectors, we need to embed them as a string query + // This is a simplification - ideally we would send the vector directly + query = 'vector-query' // Placeholder, would need a better approach for vector queries + } + + if (!this.serverSearchConduit || !this.serverConnection) { + throw new Error( + 'Server search conduit or connection is not initialized' + ) + } + + // Search the remote server + const searchResult = await this.serverSearchConduit.searchServer( + this.serverConnection.connectionId, + query, + k + ) + + if (!searchResult.success) { + throw new Error(`Remote search failed: ${searchResult.error}`) + } + + return searchResult.data as SearchResult[] + } catch (error) { + console.error('Failed to search remote server:', error) + throw new Error(`Failed to search remote server: ${error}`) + } + } + + /** + * Search both local and remote Brainy instances, combining the results + * @param queryVectorOrData Query vector or data to search for + * @param k Number of results to return + * @param options Additional options + * @returns Array of search results + */ + public async searchCombined( + queryVectorOrData: Vector | any, + k: number = 10, + options: { + forceEmbed?: boolean // Force using the embedding function even if input is a vector + nounTypes?: string[] // Optional array of noun types to search within + includeVerbs?: boolean // Whether to include associated GraphVerbs in the results + localFirst?: boolean // Whether to search local first (default: true) + } = {} + ): Promise[]> { + await this.ensureInitialized() + + // Check if connected to a remote server + if (!this.isConnectedToRemoteServer()) { + // If not connected to a remote server, just search locally + return this.searchLocal(queryVectorOrData, k, options) + } + + try { + // Default to searching local first + const localFirst = options.localFirst !== false + + if (localFirst) { + // Search local first + const localResults = await this.searchLocal( + queryVectorOrData, + k, + options + ) + + // If we have enough local results, return them + if (localResults.length >= k) { + return localResults + } + + // Otherwise, search remote for additional results + const remoteResults = await this.searchRemote( + queryVectorOrData, + k - localResults.length, + { ...options, storeResults: true } + ) + + // Combine results, removing duplicates + const combinedResults = [...localResults] + const localIds = new Set(localResults.map((r) => r.id)) + + for (const result of remoteResults) { + if (!localIds.has(result.id)) { + combinedResults.push(result) + } + } + + return combinedResults + } else { + // Search remote first + const remoteResults = await this.searchRemote(queryVectorOrData, k, { + ...options, + storeResults: true + }) + + // If we have enough remote results, return them + if (remoteResults.length >= k) { + return remoteResults + } + + // Otherwise, search local for additional results + const localResults = await this.searchLocal( + queryVectorOrData, + k - remoteResults.length, + options + ) + + // Combine results, removing duplicates + const combinedResults = [...remoteResults] + const remoteIds = new Set(remoteResults.map((r) => r.id)) + + for (const result of localResults) { + if (!remoteIds.has(result.id)) { + combinedResults.push(result) + } + } + + return combinedResults + } + } catch (error) { + console.error('Failed to perform combined search:', error) + throw new Error(`Failed to perform combined search: ${error}`) + } + } + + /** + * Check if the instance is connected to a remote server + * @returns True if connected to a remote server, false otherwise + */ + public isConnectedToRemoteServer(): boolean { + return !!(this.serverSearchConduit && this.serverConnection) + } + + /** + * Disconnect from the remote server + * @returns True if successfully disconnected, false if not connected + */ + public async disconnectFromRemoteServer(): Promise { + if (!this.isConnectedToRemoteServer()) { + return false + } + + try { + if (!this.serverSearchConduit || !this.serverConnection) { + throw new Error( + 'Server search conduit or connection is not initialized' + ) + } + + // Close the WebSocket connection + await this.serverSearchConduit.closeWebSocket( + this.serverConnection.connectionId + ) + + // Clear the connection information + this.serverSearchConduit = null + this.serverConnection = null + + return true + } catch (error) { + console.error('Failed to disconnect from remote server:', error) + throw new Error(`Failed to disconnect from remote server: ${error}`) + } + } + + /** + * Ensure the database is initialized + */ + private async ensureInitialized(): Promise { + if (!this.isInitialized) { + await this.init() + } + } + + /** + * Get information about the current storage usage and capacity + * @returns Object containing the storage type, used space, quota, and additional details + */ + public async status(): Promise<{ + type: string + used: number + quota: number | null + details?: Record + }> { + await this.ensureInitialized() + + if (!this.storage) { + return { + type: 'any', + used: 0, + quota: null, + details: { error: 'Storage not initialized' } + } + } + + try { + // Check if the storage adapter has a getStorageStatus method + if (typeof this.storage.getStorageStatus !== 'function') { + // If not, determine the storage type based on the constructor name + const storageType = this.storage.constructor.name + .toLowerCase() + .replace('storage', '') + return { + type: storageType || 'any', + used: 0, + quota: null, + details: { + error: 'Storage adapter does not implement getStorageStatus method', + storageAdapter: this.storage.constructor.name, + indexSize: this.size() + } + } + } + + // Get storage status from the storage adapter + const storageStatus = await this.storage.getStorageStatus() + + // Add index information to the details + let indexInfo: Record = { + indexSize: this.size() + } + + // Add optimized index information if using optimized index + if (this.useOptimizedIndex && this.index instanceof HNSWIndexOptimized) { + const optimizedIndex = this.index as HNSWIndexOptimized + indexInfo = { + ...indexInfo, + optimized: true, + memoryUsage: optimizedIndex.getMemoryUsage(), + productQuantization: optimizedIndex.getUseProductQuantization(), + diskBasedIndex: optimizedIndex.getUseDiskBasedIndex() + } + } else { + indexInfo.optimized = false + } + + // Ensure all required fields are present + return { + type: storageStatus.type || 'any', + used: storageStatus.used || 0, + quota: storageStatus.quota || null, + details: { + ...(storageStatus.details || {}), + index: indexInfo + } + } + } catch (error) { + console.error('Failed to get storage status:', error) + + // Determine the storage type based on the constructor name + const storageType = this.storage.constructor.name + .toLowerCase() + .replace('storage', '') + + return { + type: storageType || 'any', + used: 0, + quota: null, + details: { + error: String(error), + storageAdapter: this.storage.constructor.name, + indexSize: this.size() + } + } + } + } + + /** + * Shut down the database and clean up resources + * This should be called when the database is no longer needed + */ + public async shutDown(): Promise { + try { + // Disconnect from remote server if connected + if (this.isConnectedToRemoteServer()) { + await this.disconnectFromRemoteServer() + } + + // Additional cleanup could be added here in the future + + this.isInitialized = false + } catch (error) { + console.error('Failed to shut down BrainyData:', error) + throw new Error(`Failed to shut down BrainyData: ${error}`) + } + } + + /** + * Backup all data from the database to a JSON-serializable format + * @returns Object containing all nouns, verbs, noun types, verb types, HNSW index, and other related data + * + * The HNSW index data includes: + * - entryPointId: The ID of the entry point for the graph + * - maxLevel: The maximum level in the hierarchical structure + * - dimension: The dimension of the vectors + * - config: Configuration parameters for the HNSW algorithm + * - connections: A serialized representation of the connections between nouns + */ + public async backup(): Promise<{ + nouns: VectorDocument[] + verbs: GraphVerb[] + nounTypes: string[] + verbTypes: string[] + version: string + hnswIndex?: { + entryPointId: string | null + maxLevel: number + dimension: number | null + config: HNSWConfig + connections: Record> + } + }> { + await this.ensureInitialized() + + try { + // Get all nouns + const nouns = await this.getAllNouns() + + // Get all verbs + const verbs = await this.getAllVerbs() + + // Get all noun types + const nounTypes = Object.values(NounType) + + // Get all verb types + const verbTypes = Object.values(VerbType) + + // Get HNSW index data + const hnswIndexData = { + entryPointId: this.index.getEntryPointId(), + maxLevel: this.index.getMaxLevel(), + dimension: this.index.getDimension(), + config: this.index.getConfig(), + connections: {} as Record> + } + + // Convert Map> to a serializable format + const indexNouns = this.index.getNouns() + for (const [id, noun] of indexNouns.entries()) { + hnswIndexData.connections[id] = {} + for (const [level, connections] of noun.connections.entries()) { + hnswIndexData.connections[id][level] = Array.from(connections) + } + } + + // Return the data with version information + return { + nouns, + verbs, + nounTypes, + verbTypes, + hnswIndex: hnswIndexData, + version: '1.0.0' // Version of the backup format + } + } catch (error) { + console.error('Failed to backup data:', error) + throw new Error(`Failed to backup data: ${error}`) + } + } + + /** + * Import sparse data into the database + * @param data The sparse data to import + * If vectors are not present for nouns, they will be created using the embedding function + * @param options Import options + * @returns Object containing counts of imported items + */ + public async importSparseData( + data: { + nouns: VectorDocument[] + verbs: GraphVerb[] + nounTypes?: string[] + verbTypes?: string[] + hnswIndex?: { + entryPointId: string | null + maxLevel: number + dimension: number | null + config: HNSWConfig + connections: Record> + } + version: string + }, + options: { + clearExisting?: boolean + } = {} + ): Promise<{ + nounsRestored: number + verbsRestored: number + }> { + return this.restore(data, options); + } + + /** + * Restore data into the database from a previously backed up format + * @param data The data to restore, in the format returned by backup() + * This can include HNSW index data if it was included in the backup + * If vectors are not present for nouns, they will be created using the embedding function + * @param options Restore options + * @returns Object containing counts of restored items + */ + public async restore( + data: { + nouns: VectorDocument[] + verbs: GraphVerb[] + nounTypes?: string[] + verbTypes?: string[] + hnswIndex?: { + entryPointId: string | null + maxLevel: number + dimension: number | null + config: HNSWConfig + connections: Record> + } + version: string + }, + options: { + clearExisting?: boolean + } = {} + ): Promise<{ + nounsRestored: number + verbsRestored: number + }> { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + try { + // Clear existing data if requested + if (options.clearExisting) { + await this.clear() + } + + // Validate the data format + if (!data || !data.nouns || !data.verbs || !data.version) { + throw new Error('Invalid restore data format') + } + + // Log additional data if present + if (data.nounTypes) { + console.log(`Found ${data.nounTypes.length} noun types in restore data`) + } + + if (data.verbTypes) { + console.log(`Found ${data.verbTypes.length} verb types in restore data`) + } + + if (data.hnswIndex) { + console.log('Found HNSW index data in backup') + } + + // Restore nouns + let nounsRestored = 0 + for (const noun of data.nouns) { + try { + // Check if the noun has a vector + if (!noun.vector || noun.vector.length === 0) { + // If no vector, create one using the embedding function + if ( + noun.metadata && + typeof noun.metadata === 'object' && + 'text' in noun.metadata + ) { + // If the metadata has a text field, use it for embedding + noun.vector = await this.embeddingFunction(noun.metadata.text) + } else { + // Otherwise, use the entire metadata for embedding + noun.vector = await this.embeddingFunction(noun.metadata) + } + } + + // Add the noun with its vector and metadata + await this.add(noun.vector, noun.metadata, { id: noun.id }) + nounsRestored++ + } catch (error) { + console.error(`Failed to restore noun ${noun.id}:`, error) + // Continue with other nouns + } + } + + // Restore verbs + let verbsRestored = 0 + for (const verb of data.verbs) { + try { + // Check if the verb has a vector + if (!verb.vector || verb.vector.length === 0) { + // If no vector, create one using the embedding function + if ( + verb.metadata && + typeof verb.metadata === 'object' && + 'text' in verb.metadata + ) { + // If the metadata has a text field, use it for embedding + verb.vector = await this.embeddingFunction(verb.metadata.text) + } else { + // Otherwise, use the entire metadata for embedding + verb.vector = await this.embeddingFunction(verb.metadata) + } + } + + // Add the verb + await this.addVerb(verb.sourceId, verb.targetId, verb.vector, { + id: verb.id, + type: verb.metadata?.verb || VerbType.RelatedTo, + metadata: verb.metadata + }) + verbsRestored++ + } catch (error) { + console.error(`Failed to restore verb ${verb.id}:`, error) + // Continue with other verbs + } + } + + // If HNSW index data is provided and we've restored nouns, reconstruct the index + if (data.hnswIndex && nounsRestored > 0) { + try { + console.log('Reconstructing HNSW index from backup data...') + + // Create a new index with the restored configuration + this.index = new HNSWIndex( + data.hnswIndex.config, + this.distanceFunction + ) + + // Re-add all nouns to the index + for (const noun of data.nouns) { + if (noun.vector && noun.vector.length > 0) { + await this.index.addItem({ id: noun.id, vector: noun.vector }) + } + } + + console.log('HNSW index reconstruction complete') + } catch (error) { + console.error('Failed to reconstruct HNSW index:', error) + console.log('Continuing with standard restore process...') + } + } + + return { + nounsRestored, + verbsRestored + } + } catch (error) { + console.error('Failed to restore data:', error) + throw new Error(`Failed to restore data: ${error}`) + } + } + + /** + * Generate a random graph of data with typed nouns and verbs for testing and experimentation + * @param options Configuration options for the random graph + * @returns Object containing the IDs of the generated nouns and verbs + */ + public async generateRandomGraph( + options: { + nounCount?: number // Number of nouns to generate (default: 10) + verbCount?: number // Number of verbs to generate (default: 20) + nounTypes?: NounType[] // Types of nouns to generate (default: all types) + verbTypes?: VerbType[] // Types of verbs to generate (default: all types) + clearExisting?: boolean // Whether to clear existing data before generating (default: false) + seed?: string // Seed for random generation (default: random) + } = {} + ): Promise<{ + nounIds: string[] + verbIds: string[] + }> { + await this.ensureInitialized() + + // Check if database is in read-only mode + this.checkReadOnly() + + // Set default options + const nounCount = options.nounCount || 10 + const verbCount = options.verbCount || 20 + const nounTypes = options.nounTypes || Object.values(NounType) + const verbTypes = options.verbTypes || Object.values(VerbType) + const clearExisting = options.clearExisting || false + + // Clear existing data if requested + if (clearExisting) { + await this.clear() + } + + try { + // Generate random nouns + const nounIds: string[] = [] + const nounDescriptions: Record = { + [NounType.Person]: 'A person with unique characteristics', + [NounType.Place]: 'A location with specific attributes', + [NounType.Thing]: 'An object with distinct properties', + [NounType.Event]: 'An occurrence with temporal aspects', + [NounType.Concept]: 'An abstract idea or notion', + [NounType.Content]: 'A piece of content or information', + [NounType.Group]: 'A collection of related entities', + [NounType.List]: 'An ordered sequence of items', + [NounType.Category]: 'A classification or grouping' + } + + for (let i = 0; i < nounCount; i++) { + // Select a random noun type + const nounType = nounTypes[Math.floor(Math.random() * nounTypes.length)] + + // Generate a random label + const label = `Random ${nounType} ${i + 1}` + + // Create metadata + const metadata = { + noun: nounType, + label, + description: nounDescriptions[nounType] || `A random ${nounType}`, + randomAttributes: { + value: Math.random() * 100, + priority: Math.floor(Math.random() * 5) + 1, + tags: [`tag-${i % 5}`, `category-${i % 3}`] + } + } + + // Add the noun + const id = await this.add(metadata.description, metadata as T) + nounIds.push(id) + } + + // Generate random verbs between nouns + const verbIds: string[] = [] + const verbDescriptions: Record = { + [VerbType.AttributedTo]: 'Attribution relationship', + [VerbType.Controls]: 'Control relationship', + [VerbType.Created]: 'Creation relationship', + [VerbType.Earned]: 'Achievement relationship', + [VerbType.Owns]: 'Ownership relationship', + [VerbType.MemberOf]: 'Membership relationship', + [VerbType.RelatedTo]: 'General relationship', + [VerbType.WorksWith]: 'Collaboration relationship', + [VerbType.FriendOf]: 'Friendship relationship', + [VerbType.ReportsTo]: 'Reporting relationship', + [VerbType.Supervises]: 'Supervision relationship', + [VerbType.Mentors]: 'Mentorship relationship' + } + + for (let i = 0; i < verbCount; i++) { + // Select random source and target nouns + const sourceIndex = Math.floor(Math.random() * nounIds.length) + let targetIndex = Math.floor(Math.random() * nounIds.length) + + // Ensure source and target are different + while (targetIndex === sourceIndex && nounIds.length > 1) { + targetIndex = Math.floor(Math.random() * nounIds.length) + } + + const sourceId = nounIds[sourceIndex] + const targetId = nounIds[targetIndex] + + // Select a random verb type + const verbType = verbTypes[Math.floor(Math.random() * verbTypes.length)] + + // Create metadata + const metadata = { + verb: verbType, + description: + verbDescriptions[verbType] || `A random ${verbType} relationship`, + weight: Math.random(), + confidence: Math.random(), + randomAttributes: { + strength: Math.random() * 100, + duration: Math.floor(Math.random() * 365) + 1, + tags: [`relation-${i % 5}`, `strength-${i % 3}`] + } + } + + // Add the verb + const id = await this.addVerb(sourceId, targetId, undefined, { + type: verbType, + weight: metadata.weight, + metadata + }) + + verbIds.push(id) + } + + return { + nounIds, + verbIds + } + } catch (error) { + console.error('Failed to generate random graph:', error) + throw new Error(`Failed to generate random graph: ${error}`) + } + } +} + +// Export distance functions for convenience +export { + euclideanDistance, + cosineDistance, + manhattanDistance, + dotProductDistance +} from './utils/index.js' diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 00000000..4ce4df09 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,1286 @@ +#!/usr/bin/env node + +/** + * Brainy CLI + * A command-line interface for interacting with the Brainy vector database + */ + +import { BrainyData, NounType, VerbType, FileSystemStorage } from './index.js' +import { fileURLToPath } from 'url' +import { dirname, join } from 'path' +import fs from 'fs' +import { Command } from 'commander' +import omelette from 'omelette' +import { VERSION } from './utils/version.js' +import { sequentialPipeline } from './sequentialPipeline.js' +import { augmentationPipeline, ExecutionMode } from './augmentationPipeline.js' +import { AugmentationType } from './types/augmentations.js' + +// Get the directory of the current module +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +// Helper function to parse JSON safely +function parseJSON(str: string): any { + try { + return JSON.parse(str) + } catch (e) { + console.error('Error parsing JSON:', (e as Error).message) + return {} + } +} + +// Helper function to resolve noun type +function resolveNounType(type: string | number | undefined): NounType { + if (!type) return NounType.Thing + + // If it's a string, try to match it to a NounType + if (typeof type === 'string') { + const nounTypeKey = Object.keys(NounType).find( + (key) => key.toLowerCase() === type.toLowerCase() + ) + return nounTypeKey + ? NounType[nounTypeKey as keyof typeof NounType] + : NounType.Thing + } + + // Convert number to string type for safety + return Object.values(NounType)[type as number] || NounType.Thing +} + +// Helper function to resolve verb type +function resolveVerbType(type: string | number | undefined): VerbType { + if (!type) return VerbType.RelatedTo + + // If it's a string, try to match it to a VerbType + if (typeof type === 'string') { + const verbTypeKey = Object.keys(VerbType).find( + (key) => key.toLowerCase() === type.toLowerCase() + ) + return verbTypeKey + ? VerbType[verbTypeKey as keyof typeof VerbType] + : VerbType.RelatedTo + } + + // Convert number to string type for safety + return Object.values(VerbType)[type as number] || VerbType.RelatedTo +} + +// Create a new Command instance +const program = new Command() + +// Configure the program +program + .name('@soulcraft/brainy') + .description( + 'A vector database using HNSW indexing with Origin Private File System storage' + ) + .version(VERSION, '-V, --version', 'Output the current version') + +// Create data directory if it doesn't exist +const dataDir = join(__dirname, '..', 'data') +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }) +} + +// Create a database instance with file system storage +const createDb = () => { + return new BrainyData({ + storageAdapter: new FileSystemStorage(dataDir) + }) +} + +// Define commands +program + .command('init') + .description('Initialize a new database') + .action(async () => { + try { + const db = createDb() + await db.init() + console.log('Database initialized successfully') + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('add') + .description('Add a new noun with the given text and optional metadata') + .argument('', 'Text to add as a noun') + .argument('[metadata]', 'Optional metadata as JSON string') + .action(async (text, metadataStr) => { + try { + const db = createDb() + await db.init() + + const metadata = metadataStr ? parseJSON(metadataStr) : {} + + // Process metadata to handle noun type + if (metadata.noun) { + metadata.noun = resolveNounType(metadata.noun) + } + + const id = await db.add(text, metadata) + console.log(`Added noun with ID: ${id}`) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('search') + .description('Search for nouns similar to the query') + .argument('', 'Search query text') + .option('-l, --limit ', 'Maximum number of results to return', '5') + .action(async (query, options) => { + try { + const db = createDb() + await db.init() + + const limit = parseInt(options.limit, 10) + const results = await db.searchText(query, limit) + + console.log(`Search results for "${query}":`) + results.forEach((result, index) => { + console.log(`${index + 1}. ID: ${result.id}`) + console.log(` Score: ${result.score.toFixed(4)}`) + console.log(` Metadata: ${JSON.stringify(result.metadata)}`) + console.log( + ` Vector: [${result.vector + .slice(0, 3) + .map((v) => v.toFixed(2)) + .join(', ')}...]` + ) + console.log() + }) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('get') + .description('Get a noun by ID') + .argument('', 'ID of the noun to get') + .action(async (id) => { + try { + const db = createDb() + await db.init() + + const noun = await db.get(id) + if (noun) { + console.log(`Noun ID: ${noun.id}`) + console.log(`Metadata: ${JSON.stringify(noun.metadata)}`) + console.log( + `Vector: [${noun.vector + .slice(0, 5) + .map((v) => v.toFixed(2)) + .join(', ')}...]` + ) + } else { + console.log(`No noun found with ID: ${id}`) + } + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('delete') + .description('Delete a noun by ID') + .argument('', 'ID of the noun to delete') + .action(async (id) => { + try { + const db = createDb() + await db.init() + + await db.delete(id) + console.log(`Deleted noun with ID: ${id}`) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('addVerb') + .description('Add a relationship between nouns') + .argument('', 'ID of the source noun') + .argument('', 'ID of the target noun') + .argument('', 'Type of relationship') + .argument('[metadata]', 'Optional metadata as JSON string') + .action(async (sourceId, targetId, verbTypeStr, metadataStr) => { + try { + const db = createDb() + await db.init() + + const verbType = resolveVerbType(verbTypeStr) + const verbMetadata = metadataStr ? parseJSON(metadataStr) : {} + + // Add verb type to metadata + verbMetadata.verb = verbType + + const verbId = await db.addVerb(sourceId, targetId, undefined, { + type: verbType, + metadata: verbMetadata + }) + console.log(`Added verb with ID: ${verbId}`) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('getVerbs') + .description('Get all relationships for a noun') + .argument('', 'ID of the noun to get relationships for') + .action(async (id) => { + try { + const db = createDb() + await db.init() + + const verbs = await db.getVerbsBySource(id) + + console.log(`Relationships for noun ${id}:`) + if (verbs.length === 0) { + console.log('No relationships found') + } else { + verbs.forEach((verb, index) => { + console.log(`${index + 1}. ID: ${verb.id}`) + console.log( + ` Type: ${Object.keys(VerbType).find((key) => VerbType[key as keyof typeof VerbType] === verb.metadata.verb) || verb.metadata.verb}` + ) + console.log(` Target: ${verb.targetId}`) + console.log(` Metadata: ${JSON.stringify(verb.metadata)}`) + console.log() + }) + } + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('status') + .description('Show database status') + .action(async () => { + try { + const db = createDb() + await db.init() + + const status = await db.status() + console.log('Database Status:') + console.log(`Storage type: ${status.type}`) + console.log(`Storage used: ${status.used} bytes`) + console.log( + `Storage quota: ${status.quota !== null ? `${status.quota} bytes` : 'unlimited'}` + ) + + // Display additional details if available + if (status.details) { + console.log('Additional details:') + Object.entries(status.details).forEach(([key, value]) => { + console.log(` ${key}: ${JSON.stringify(value)}`) + }) + } + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('clear') + .description('Clear all data from the database') + .option('-f, --force', 'Skip confirmation prompt', false) + .action(async (options) => { + try { + // Confirm unless --force is used + if (!options.force) { + console.log( + 'WARNING: This will permanently delete ALL data in the database.' + ) + console.log('To proceed without confirmation, use the --force option.') + + // Exit without doing anything + console.log('Operation cancelled. No data was deleted.') + console.log('To clear all data, use: brainy clear --force') + return + } + + const db = createDb() + await db.init() + + await db.clear() + console.log('Database cleared successfully. All data has been removed.') + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('backup') + .description('Backup all data from the database to a JSON file') + .argument('[filename]', 'Output filename (default: brainy-backup.json)') + .action(async (filename) => { + try { + const db = createDb() + await db.init() + + // Default filename if not provided + const outputFile = filename || 'brainy-backup.json' + + // Backup the data + const data = await db.backup() + + // Write to file + fs.writeFileSync(outputFile, JSON.stringify(data, null, 2)) + + console.log(`Data backed up successfully to ${outputFile}`) + console.log( + `Backed up ${data.nouns.length} nouns and ${data.verbs.length} verbs` + ) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('restore') + .description('Restore data from a JSON file into the database') + .argument('', 'Input JSON file') + .option('-c, --clear', 'Clear existing data before restoring', false) + .action(async (filename, options) => { + try { + const db = createDb() + await db.init() + + // Read the file + if (!fs.existsSync(filename)) { + console.error(`File not found: ${filename}`) + process.exit(1) + } + + const fileContent = fs.readFileSync(filename, 'utf8') + const data = JSON.parse(fileContent) + + // Restore the data + const result = await db.restore(data, { clearExisting: options.clear }) + + console.log(`Data restored successfully from ${filename}`) + console.log( + `Restored ${result.nounsRestored} nouns and ${result.verbsRestored} verbs` + ) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('import-sparse') + .description( + 'Import sparse data (without vectors) from a JSON file into the database' + ) + .argument('', 'Input JSON file') + .option('-c, --clear', 'Clear existing data before importing', false) + .action(async (filename, options) => { + try { + const db = createDb() + await db.init() + + // Read the file + if (!fs.existsSync(filename)) { + console.error(`File not found: ${filename}`) + process.exit(1) + } + + const fileContent = fs.readFileSync(filename, 'utf8') + const data = JSON.parse(fileContent) + + // Import the sparse data + const result = await db.importSparseData(data, { + clearExisting: options.clear + }) + + console.log(`Sparse data imported successfully from ${filename}`) + console.log( + `Imported ${result.nounsRestored} nouns and ${result.verbsRestored} verbs` + ) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('visualize') + .description('Visualize the graph structure in ASCII format') + .option('-r, --root ', 'ID of the root noun to start visualization from') + .option( + '-d, --depth ', + 'Maximum depth of the graph to visualize', + '2' + ) + .option('-t, --type ', 'Filter by noun type') + .option( + '-l, --limit ', + 'Maximum number of nodes to display per level', + '10' + ) + .action(async (options) => { + try { + const db = createDb() + await db.init() + + // Parse options + const depth = parseInt(options.depth, 10) + const limit = parseInt(options.limit, 10) + const rootId = options.root + const nounType = options.type ? resolveNounType(options.type) : undefined + + // Get all nouns if no root is specified + if (!rootId && !nounType) { + // Get all nouns (limited by the limit option) + const allNouns = [] + let count = 0 + + // Since there's no direct method to get all nouns, we'll use search with a high limit + const searchResults = await db.search('', 1000, { + forceEmbed: true + }) + + for (const result of searchResults) { + if (count >= limit) break + allNouns.push(result) + count++ + } + + if (allNouns.length === 0) { + console.log('No nouns found in the database.') + return + } + + console.log(`Graph Overview (showing ${allNouns.length} nouns):\n`) + + for (const noun of allNouns) { + // Get outgoing verbs + const outgoingVerbs = await db.getVerbsBySource(noun.id) + // Get incoming verbs + const incomingVerbs = await db.getVerbsByTarget(noun.id) + + const nounType = noun.metadata?.noun || 'Unknown' + const label = noun.metadata?.label || noun.id.substring(0, 8) + + console.log(`[${nounType}] ${label} (${noun.id})`) + + if (outgoingVerbs.length > 0) { + console.log(' Outgoing:') + for (const verb of outgoingVerbs.slice(0, limit)) { + const targetNoun = await db.get(verb.targetId) + const targetLabel = + targetNoun?.metadata?.label || verb.targetId.substring(0, 8) + console.log( + ` --(${verb.metadata?.verb || 'relates to'})--→ [${targetNoun?.metadata?.noun || 'Unknown'}] ${targetLabel}` + ) + } + if (outgoingVerbs.length > limit) { + console.log(` ... and ${outgoingVerbs.length - limit} more`) + } + } + + if (incomingVerbs.length > 0) { + console.log(' Incoming:') + for (const verb of incomingVerbs.slice(0, limit)) { + const sourceNoun = await db.get(verb.sourceId) + const sourceLabel = + sourceNoun?.metadata?.label || verb.sourceId.substring(0, 8) + console.log( + ` ←--(${verb.metadata?.verb || 'relates to'})-- [${sourceNoun?.metadata?.noun || 'Unknown'}] ${sourceLabel}` + ) + } + if (incomingVerbs.length > limit) { + console.log(` ... and ${incomingVerbs.length - limit} more`) + } + } + + console.log('') + } + + return + } + + // If noun type is specified but no root, show all nouns of that type + if (!rootId && nounType) { + console.log(`Visualizing nouns of type: ${nounType}\n`) + + // Search for nouns of the specified type + const searchResults = await db.search('', 1000, { + nounTypes: [nounType], + forceEmbed: true + }) + + const filteredNouns = searchResults.slice(0, limit) + + if (filteredNouns.length === 0) { + console.log(`No nouns found with type: ${nounType}`) + return + } + + for (const noun of filteredNouns) { + const label = noun.metadata?.label || noun.id.substring(0, 8) + + console.log(`[${nounType}] ${label} (${noun.id})`) + + // Get outgoing verbs + const outgoingVerbs = await db.getVerbsBySource(noun.id) + if (outgoingVerbs.length > 0) { + console.log(' Outgoing:') + for (const verb of outgoingVerbs.slice(0, limit)) { + const targetNoun = await db.get(verb.targetId) + const targetLabel = + targetNoun?.metadata?.label || verb.targetId.substring(0, 8) + console.log( + ` --(${verb.metadata?.verb || 'relates to'})--→ [${targetNoun?.metadata?.noun || 'Unknown'}] ${targetLabel}` + ) + } + if (outgoingVerbs.length > limit) { + console.log(` ... and ${outgoingVerbs.length - limit} more`) + } + } + + console.log('') + } + + return + } + + // If root is specified, visualize the graph starting from that root + if (rootId) { + const rootNoun = await db.get(rootId) + if (!rootNoun) { + console.error(`Root noun with ID ${rootId} not found`) + return + } + + console.log( + `Visualizing graph from root: ${rootNoun.metadata?.label || rootId}\n` + ) + + // Use a breadth-first search to visualize the graph + const visited = new Set() + const queue: Array<{ id: string; level: number; path: string }> = [ + { id: rootId, level: 0, path: '' } + ] + + while (queue.length > 0) { + const { id, level, path } = queue.shift()! + + if (visited.has(id) || level > depth) { + continue + } + + visited.add(id) + + const noun = await db.get(id) + if (!noun) { + console.warn(`Noun with ID ${id} not found`) + continue + } + + const nounType = noun.metadata?.noun || 'Unknown' + const label = noun.metadata?.label || id.substring(0, 8) + + // Print the current noun with proper indentation + console.log( + `${' '.repeat(level * 2)}${path}[${nounType}] ${label} (${id})` + ) + + // Get outgoing verbs + const outgoingVerbs = await db.getVerbsBySource(id) + + // Add target nouns to the queue for the next level + let verbCount = 0 + for (const verb of outgoingVerbs) { + if (verbCount >= limit) { + console.log( + `${' '.repeat((level + 1) * 2)}... and ${outgoingVerbs.length - limit} more` + ) + break + } + + const verbType = verb.metadata?.verb || 'relates to' + queue.push({ + id: verb.targetId, + level: level + 1, + path: `--(${verbType})--→ ` + }) + verbCount++ + } + } + } + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +program + .command('generate-random-graph') + .description( + 'Generate a random graph of data with typed nouns and verbs for testing' + ) + .option('-n, --noun-count ', 'Number of nouns to generate', '10') + .option('-v, --verb-count ', 'Number of verbs to generate', '20') + .option('-c, --clear', 'Clear existing data before generating', false) + .option( + '-t, --noun-types ', + 'Comma-separated list of noun types to use' + ) + .option( + '-r, --verb-types ', + 'Comma-separated list of verb types to use' + ) + .action(async (options) => { + try { + const db = createDb() + await db.init() + + // Parse options + const nounCount = parseInt(options.nounCount, 10) + const verbCount = parseInt(options.verbCount, 10) + const clearExisting = options.clear + + // Parse noun types if provided + let nounTypes: NounType[] | undefined + if (options.nounTypes) { + const typeNames = options.nounTypes + .split(',') + .map((t: string) => t.trim()) + nounTypes = typeNames + .map((name: string) => { + // Try to match by key name (case insensitive) + const key = Object.keys(NounType).find( + (k) => k.toLowerCase() === name.toLowerCase() + ) + if (key) return NounType[key as keyof typeof NounType] + + // If not found by key, check if it's a valid value + if (Object.values(NounType).includes(name as NounType)) { + return name as NounType + } + + console.warn(`Warning: Unknown noun type "${name}", ignoring`) + return null + }) + .filter(Boolean) as NounType[] + } + + // Parse verb types if provided + let verbTypes: VerbType[] | undefined + if (options.verbTypes) { + const typeNames = options.verbTypes + .split(',') + .map((t: string) => t.trim()) + verbTypes = typeNames + .map((name: string) => { + // Try to match by key name (case insensitive) + const key = Object.keys(VerbType).find( + (k) => k.toLowerCase() === name.toLowerCase() + ) + if (key) return VerbType[key as keyof typeof VerbType] + + // If not found by key, check if it's a valid value + if (Object.values(VerbType).includes(name as VerbType)) { + return name as VerbType + } + + console.warn(`Warning: Unknown verb type "${name}", ignoring`) + return null + }) + .filter(Boolean) as VerbType[] + } + + console.log( + `Generating random graph with ${nounCount} nouns and ${verbCount} verbs...` + ) + if (clearExisting) { + console.log('Clearing existing data first...') + } + + const result = await db.generateRandomGraph({ + nounCount, + verbCount, + nounTypes, + verbTypes, + clearExisting + }) + + console.log('Random graph generated successfully!') + console.log( + `Created ${result.nounIds.length} nouns and ${result.verbIds.length} verbs` + ) + + // Print some sample IDs + if (result.nounIds.length > 0) { + console.log('\nSample noun IDs:') + result.nounIds.slice(0, 3).forEach((id) => console.log(`- ${id}`)) + if (result.nounIds.length > 3) { + console.log(`... and ${result.nounIds.length - 3} more`) + } + } + + if (result.verbIds.length > 0) { + console.log('\nSample verb IDs:') + result.verbIds.slice(0, 3).forEach((id) => console.log(`- ${id}`)) + if (result.verbIds.length > 3) { + console.log(`... and ${result.verbIds.length - 3} more`) + } + } + + console.log( + '\nUse the search, get, or visualize commands to explore the generated graph' + ) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +// Add demo to help text +program.addHelpText( + 'after', + ` +Examples: + $ brainy init + $ brainy add "Cats are independent pets" '{"noun":"Thing","category":"animal"}' + $ brainy search "feline pets" --limit 5 + $ brainy addVerb id1 id2 RelatedTo '{"description":"Both are pets"}' + $ brainy clear --force + $ brainy generate-random-graph --noun-count 20 --verb-count 30 --clear + $ brainy generate-random-graph --noun-types Person,Thing --verb-types RelatedTo,Owns + $ brainy visualize --type Thing --limit 10 + $ brainy visualize --root id1 --depth 3 + $ brainy backup my-database-backup.json + $ brainy restore my-database-backup.json --clear + + # Augmentation commands + $ brainy augment list + $ brainy augment info cognition + $ brainy augment test-pipeline "Test data" --data-type text --mode sequential + $ brainy augment stream-test --count 3 --interval 500 + +` +) + +// Setup autocomplete +const completion = omelette('brainy') + +// Helper function to get all noun types +const getNounTypes = () => Object.keys(NounType) + +// Helper function to get all verb types +const getVerbTypes = () => Object.keys(VerbType) + +// Define autocomplete handlers +completion.tree({ + // First level commands - suggest all available commands + _: () => [ + 'add', + 'addVerb', + 'search', + 'get', + 'delete', + 'getVerbs', + 'status', + 'clear', + 'visualize', + 'generate-random-graph', + 'backup', + 'restore', + 'import-sparse', + 'completion-setup', + 'init', + 'help', + 'augment' + ], + // Command-specific completions + add: { + // For the second argument of 'add' command (metadata) + _: () => { + // Generate templates for each noun type + return getNounTypes().map( + (type) => `{"noun":"${type}","category":"example"}` + ) + } + }, + addVerb: { + // First two arguments are IDs, third is verb type + '': { + '': { + _: () => { + // Suggest all available verb types + return getVerbTypes() + } + } + } + }, + // Add autocomplete for other commands + search: {}, + get: {}, + delete: {}, + getVerbs: {}, + status: {}, + clear: { + _: () => ['--force'] + }, + backup: { + _: () => ['brainy-backup.json', 'database-backup.json'] + }, + restore: { + _: () => ['--clear'] + }, + 'import-sparse': { + _: () => ['--clear'] + }, + 'generate-random-graph': { + _: () => [ + '--noun-count 10', + '--verb-count 20', + '--clear', + `--noun-types ${getNounTypes().join(',')}`, + `--verb-types ${getVerbTypes().join(',')}` + ] + }, + augment: { + _: () => ['list', 'info', 'test-pipeline', 'stream-test'], + info: { + _: () => [ + 'sense', + 'memory', + 'cognition', + 'conduit', + 'activation', + 'perception', + 'dialog', + 'websocket' + ] + }, + 'test-pipeline': { + _: () => [ + '--data-type text', + '--mode sequential', + '--mode parallel', + '--mode threaded', + '--stop-on-error', + '--verbose' + ] + }, + 'stream-test': { + _: () => ['--count 5', '--interval 1000', '--data-type text', '--verbose'] + } + }, + 'completion-setup': {}, + init: {}, + help: {} +}) + +// Initialize autocomplete +completion.init() + +// If this script is run with --completion-setup flag, set up the autocomplete +if (process.argv.includes('--completion-setup')) { + completion.setupShellInitFile() + console.log('Autocomplete setup complete. Please restart your shell.') + process.exit(0) +} + +// Pipeline and Augmentation Commands +const augmentCommand = new Command('augment').description( + 'Augmentation pipeline operations' +) + +augmentCommand + .command('list') + .description( + 'List all available augmentation types and registered augmentations' + ) + .action(async () => { + try { + // Initialize the pipeline + await augmentationPipeline.initialize() + + // Get available augmentation types + const availableTypes = + augmentationPipeline.getAvailableAugmentationTypes() + + console.log('Available Augmentation Types:') + if (availableTypes.length === 0) { + console.log(' No augmentation types available') + } else { + availableTypes.forEach((type) => { + const augmentations = + augmentationPipeline.getAugmentationsByType(type) + console.log( + `\n${type.toUpperCase()} (${augmentations.length} registered):` + ) + + if (augmentations.length === 0) { + console.log(' No augmentations registered for this type') + } else { + augmentations.forEach((aug) => { + console.log(` - ${aug.name}: ${aug.description}`) + console.log(` Status: ${aug.enabled ? 'Enabled' : 'Disabled'}`) + }) + } + }) + } + + // Show WebSocket augmentations separately + const webSocketAugs = augmentationPipeline.getWebSocketAugmentations() + console.log('\nWebSocket-Enabled Augmentations:') + if (webSocketAugs.length === 0) { + console.log(' No WebSocket-enabled augmentations available') + } else { + webSocketAugs.forEach((aug) => { + console.log(` - ${aug.name}: ${aug.description}`) + console.log(` Status: ${aug.enabled ? 'Enabled' : 'Disabled'}`) + }) + } + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +augmentCommand + .command('test-pipeline') + .description('Test the sequential pipeline with sample data') + .argument( + '[text]', + 'Sample text to process through the pipeline', + 'This is a test of the Brainy pipeline' + ) + .option('-t, --data-type ', 'Type of data to process', 'text') + .option( + '-m, --mode ', + 'Execution mode (sequential, parallel, threaded)', + 'sequential' + ) + .option('-s, --stop-on-error', 'Stop execution if an error occurs', false) + .option('-v, --verbose', 'Show detailed output', false) + .action(async (text, options) => { + try { + // Initialize the pipeline + await sequentialPipeline.initialize() + + console.log(`Processing data: "${text}"`) + console.log(`Data type: ${options.dataType}`) + console.log(`Execution mode: ${options.mode}`) + console.log(`Stop on error: ${options.stopOnError}`) + console.log() + + // Set execution mode + let executionMode = ExecutionMode.SEQUENTIAL + switch (options.mode.toLowerCase()) { + case 'parallel': + executionMode = ExecutionMode.PARALLEL + break + case 'threaded': + executionMode = ExecutionMode.THREADED + break + default: + executionMode = ExecutionMode.SEQUENTIAL + } + + // Process the data + const result = await sequentialPipeline.processData( + text, + options.dataType, + { + stopOnError: options.stopOnError, + timeout: 30000 + } + ) + + console.log('Pipeline Execution Result:') + console.log(`Success: ${result.success}`) + + if (result.error) { + console.log(`Error: ${result.error}`) + } + + console.log('\nStage Results:') + + // Display stage results + Object.entries(result.stageResults).forEach(([stage, stageResult]) => { + console.log(`\n${stage.toUpperCase()}:`) + console.log(` Success: ${stageResult?.success}`) + + if (stageResult?.error) { + console.log(` Error: ${stageResult.error}`) + } + + if (stageResult?.data && options.verbose) { + console.log(' Data:') + console.log( + JSON.stringify(stageResult.data, null, 2) + .split('\n') + .map((line) => ` ${line}`) + .join('\n') + ) + } + }) + + console.log('\nFinal Result Data:') + console.log( + JSON.stringify(result.data, null, 2) + .split('\n') + .map((line) => ` ${line}`) + .join('\n') + ) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +augmentCommand + .command('stream-test') + .description('Test streaming data through the pipeline (simulated)') + .option('-c, --count ', 'Number of data items to stream', '5') + .option( + '-i, --interval ', + 'Interval between data items in milliseconds', + '1000' + ) + .option('-t, --data-type ', 'Type of data to process', 'text') + .option('-v, --verbose', 'Show detailed output', false) + .action(async (options) => { + try { + // Initialize the pipeline + await sequentialPipeline.initialize() + + const count = parseInt(options.count, 10) + const interval = parseInt(options.interval, 10) + + console.log( + `Simulating stream of ${count} data items at ${interval}ms intervals` + ) + console.log(`Data type: ${options.dataType}`) + console.log() + + // Create a handler function similar to what would be used with WebSockets + const handler = (data: string) => { + // Process the data asynchronously without blocking + sequentialPipeline + .processData(data, options.dataType, { stopOnError: false }) + .then((result) => { + console.log(`\nProcessed: "${data}"`) + console.log(`Success: ${result.success}`) + + if (options.verbose) { + console.log('Stage Results:') + Object.entries(result.stageResults).forEach( + ([stage, stageResult]) => { + if (stageResult?.success) { + console.log(` ${stage}: Success`) + } else { + console.log( + ` ${stage}: Failed - ${stageResult?.error || 'Unknown error'}` + ) + } + } + ) + } + + if (result.data) { + console.log('Result Data:') + console.log( + JSON.stringify(result.data, null, 2) + .split('\n') + .map((line) => ` ${line}`) + .join('\n') + ) + } + }) + .catch((error) => { + console.error(`Error processing "${data}":`, error.message) + }) + } + + // Generate sample data items + const sampleTexts = [ + 'The quick brown fox jumps over the lazy dog', + 'Artificial intelligence is transforming how we interact with data', + 'Vector databases enable semantic search capabilities', + 'Graph relationships connect entities in meaningful ways', + 'Streaming data requires efficient real-time processing', + 'WebSockets provide bidirectional communication channels', + 'Augmentations extend the functionality of the core system', + 'Sequential pipelines process data in defined stages', + 'Parallel execution improves throughput for large datasets', + 'Threaded operations utilize multiple CPU cores efficiently' + ] + + // Simulate streaming data + console.log('Starting simulated data stream...') + + for (let i = 0; i < count; i++) { + // Use modulo to cycle through sample texts if count > samples + const text = sampleTexts[i % sampleTexts.length] + + // Wait for the specified interval + if (i > 0) { + await new Promise((resolve) => setTimeout(resolve, interval)) + } + + console.log(`\nStreaming item ${i + 1}/${count}: "${text}"`) + + // Process the data + handler(text) + } + + console.log( + '\nSimulated stream complete. Some processing may still be ongoing.' + ) + console.log( + 'In a real WebSocket scenario, the connection would remain open for continuous data.' + ) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +augmentCommand + .command('info') + .description('Get detailed information about a specific augmentation type') + .argument( + '', + 'Augmentation type (sense, memory, cognition, conduit, activation, perception, dialog, websocket)' + ) + .action(async (typeArg) => { + try { + // Initialize the pipeline + await augmentationPipeline.initialize() + + // Resolve the augmentation type + let augType: AugmentationType | undefined + + // Convert input to proper enum value + const normalizedType = typeArg.toLowerCase() + switch (normalizedType) { + case 'sense': + augType = AugmentationType.SENSE + break + case 'memory': + augType = AugmentationType.MEMORY + break + case 'cognition': + augType = AugmentationType.COGNITION + break + case 'conduit': + augType = AugmentationType.CONDUIT + break + case 'activation': + augType = AugmentationType.ACTIVATION + break + case 'perception': + augType = AugmentationType.PERCEPTION + break + case 'dialog': + augType = AugmentationType.DIALOG + break + case 'websocket': + augType = AugmentationType.WEBSOCKET + break + default: + console.error(`Unknown augmentation type: ${typeArg}`) + console.log( + 'Available types: sense, memory, cognition, conduit, activation, perception, dialog, websocket' + ) + process.exit(1) + } + + // Get augmentations of the specified type + const augmentations = augmentationPipeline.getAugmentationsByType(augType) + + console.log(`\n${augType.toUpperCase()} Augmentation Details:`) + + if (augmentations.length === 0) { + console.log(' No augmentations registered for this type') + } else { + // Display information about each augmentation + augmentations.forEach((aug, index) => { + console.log(`\n${index + 1}. ${aug.name}`) + console.log(` Description: ${aug.description}`) + console.log(` Status: ${aug.enabled ? 'Enabled' : 'Disabled'}`) + + // List available methods + console.log(' Available Methods:') + + // Get all methods that aren't from Object.prototype + const methods = Object.getOwnPropertyNames( + Object.getPrototypeOf(aug) + ).filter( + (method) => + method !== 'constructor' && + typeof (aug as any)[method] === 'function' && + !['initialize', 'shutDown', 'getStatus'].includes(method) + ) + + if (methods.length === 0) { + console.log(' No custom methods available') + } else { + methods.forEach((method) => { + console.log(` - ${method}`) + }) + } + }) + } + + // Show pipeline order information + console.log('\nPipeline Execution Order:') + console.log( + ' 1. SENSE - Process raw data into structured nouns and verbs' + ) + console.log(' 2. MEMORY - Store and retrieve data') + console.log(' 3. COGNITION - Analyze and reason about data') + console.log(' 4. CONDUIT - Exchange data with external systems') + console.log(' 5. ACTIVATION - Trigger actions based on data') + console.log(' 6. PERCEPTION - Interpret and visualize data') + console.log(' 7. DIALOG - Process natural language interactions') + console.log( + ' * WEBSOCKET - Enable real-time communication (can be combined with other types)' + ) + } catch (error) { + console.error('Error:', (error as Error).message) + process.exit(1) + } + }) + +// Add the augment command to the program +program.addCommand(augmentCommand) + +// Add a command for setting up autocomplete +program + .command('completion-setup') + .description('Setup shell autocomplete for the Brainy CLI') + .action(() => { + completion.setupShellInitFile() + console.log('Autocomplete setup complete. Please restart your shell.') + }) + +// Parse command line arguments + +program.parse() diff --git a/src/coreTypes.ts b/src/coreTypes.ts new file mode 100644 index 00000000..a85f0c75 --- /dev/null +++ b/src/coreTypes.ts @@ -0,0 +1,163 @@ +/** + * Type definitions for the Soulcraft Brainy + */ + +/** + * Vector representation - an array of numbers + */ +export type Vector = number[] + +/** + * A document with a vector embedding and optional metadata + */ +export interface VectorDocument { + id: string + vector: Vector + metadata?: T +} + +/** + * Search result with similarity score + */ +export interface SearchResult { + id: string + score: number + vector: Vector + metadata?: T +} + +/** + * Distance function for comparing vectors + */ +export type DistanceFunction = (a: Vector, b: Vector) => number + +/** + * Embedding function for converting data to vectors + */ +export type EmbeddingFunction = (data: any) => Promise + +/** + * Embedding model interface + */ +export interface EmbeddingModel { + /** + * Initialize the embedding model + */ + init(): Promise + + /** + * Embed data into a vector + */ + embed(data: any): Promise + + /** + * Dispose of the model resources + */ + dispose(): Promise +} + +/** + * HNSW graph noun + */ +export interface HNSWNoun { + id: string + vector: Vector + connections: Map> // level -> set of connected noun ids +} + +/** + * Verb representing a relationship between nouns + * Extends HNSWNoun to allow verbs to be first-class entities in the data model + */ +export interface GraphVerb extends HNSWNoun { + sourceId: string // ID of the source noun + targetId: string // ID of the target noun + type?: string // Optional type of the relationship + weight?: number // Optional weight of the relationship + metadata?: any // Optional metadata for the verb + + // Additional properties used in the codebase + source?: string // Alias for sourceId + target?: string // Alias for targetId + verb?: string // Alias for type + data?: Record // Additional flexible data storage + embedding?: Vector // Vector representation of the relationship +} + +/** + * HNSW index configuration + */ +export interface HNSWConfig { + M: number // Maximum number of connections per noun + efConstruction: number // Size of the dynamic candidate list during construction + efSearch: number // Size of the dynamic candidate list during search + ml: number // Maximum level +} + +/** + * Storage interface for persistence + */ +export interface StorageAdapter { + init(): Promise + + saveNoun(noun: HNSWNoun): Promise + + getNoun(id: string): Promise + + getAllNouns(): Promise + + /** + * Get nouns by noun type + * @param nounType The noun type to filter by + * @returns Promise that resolves to an array of nouns of the specified noun type + */ + getNounsByNounType(nounType: string): Promise + + deleteNoun(id: string): Promise + + saveVerb(verb: GraphVerb): Promise + + getVerb(id: string): Promise + + getAllVerbs(): Promise + + getVerbsBySource(sourceId: string): Promise + + getVerbsByTarget(targetId: string): Promise + + getVerbsByType(type: string): Promise + + deleteVerb(id: string): Promise + + saveMetadata(id: string, metadata: any): Promise + + getMetadata(id: string): Promise + + clear(): Promise + + /** + * Get information about storage usage and capacity + * @returns Promise that resolves to an object containing storage status information + */ + getStorageStatus(): Promise<{ + /** + * The type of storage being used (e.g., 'filesystem', 'opfs', 'memory') + */ + type: string + + /** + * The amount of storage being used in bytes + */ + used: number + + /** + * The total amount of storage available in bytes, or null if unknown + */ + quota: number | null + + /** + * Additional storage-specific information + */ + details?: Record + }> +} diff --git a/src/examples/basicUsage.ts b/src/examples/basicUsage.ts new file mode 100644 index 00000000..83f881c4 --- /dev/null +++ b/src/examples/basicUsage.ts @@ -0,0 +1,163 @@ +/** + * Basic usage example for the Soulcraft Brainy database + */ + +import { BrainyData } from '../brainyData.js' + +// Example data - word embeddings +const wordEmbeddings = { + cat: [0.2, 0.3, 0.4, 0.1], + dog: [0.3, 0.2, 0.4, 0.2], + fish: [0.1, 0.1, 0.8, 0.2], + bird: [0.1, 0.4, 0.2, 0.5], + tiger: [0.3, 0.4, 0.3, 0.1], + lion: [0.4, 0.3, 0.2, 0.1], + shark: [0.2, 0.1, 0.7, 0.3], + eagle: [0.2, 0.5, 0.1, 0.4] +} + +// Example metadata +const metadata = { + cat: { type: 'mammal', domesticated: true }, + dog: { type: 'mammal', domesticated: true }, + fish: { type: 'fish', domesticated: false }, + bird: { type: 'bird', domesticated: false }, + tiger: { type: 'mammal', domesticated: false }, + lion: { type: 'mammal', domesticated: false }, + shark: { type: 'fish', domesticated: false }, + eagle: { type: 'bird', domesticated: false } +} + +/** + * Run the example + */ +async function runExample() { + console.log('Initializing vector database...') + + // Create a new vector database + const db = new BrainyData() + await db.init() + + console.log('Adding vectors to the database...') + + // Add vectors to the database + const ids: Record = {} + const metadata: Record = { + cat: { type: 'mammal', domesticated: true }, + dog: { type: 'mammal', domesticated: true }, + fish: { type: 'fish', domesticated: false }, + bird: { type: 'bird', domesticated: false }, + tiger: { type: 'mammal', domesticated: false }, + lion: { type: 'mammal', domesticated: false }, + shark: { type: 'fish', domesticated: false }, + eagle: { type: 'bird', domesticated: false } + } + for (const [word, vector] of Object.entries(wordEmbeddings)) { + ids[word] = await db.add(vector, metadata[word]) + + console.log(`Added "${word}" with ID: ${ids[word]}`) + } + + console.log('\nDatabase size:', db.size()) + + // Search for similar vectors + console.log('\nSearching for vectors similar to "cat"...') + const catResults = await db.search(wordEmbeddings['cat'], 3) + console.log('Results:') + for (const result of catResults) { + const word = + Object.entries(ids).find(([_, id]) => id === result.id)?.[0] || 'unknown' + console.log( + `- ${word} (score: ${result.score.toFixed(4)}, metadata:`, + result.metadata, + ')' + ) + } + + // Search for similar vectors + console.log('\nSearching for vectors similar to "fish"...') + const fishResults = await db.search(wordEmbeddings['fish'], 3) + console.log('Results:') + for (const result of fishResults) { + const word = + Object.entries(ids).find(([_, id]) => id === result.id)?.[0] || 'unknown' + console.log( + `- ${word} (score: ${result.score.toFixed(4)}, metadata:`, + result.metadata, + ')' + ) + } + + // Update metadata + console.log('\nUpdating metadata for "bird"...') + await db.updateMetadata(ids['bird'], { + ...metadata['bird'], + notes: 'Can fly' + }) + + // Get the updated document + const birdDoc = await db.get(ids['bird']) + console.log('Updated bird document:', birdDoc) + + // Delete a vector + console.log('\nDeleting "shark"...') + await db.delete(ids['shark']) + console.log('Database size after deletion:', db.size()) + + // Search again to verify shark is gone + console.log('\nSearching for vectors similar to "fish" after deletion...') + const fishResultsAfterDeletion = await db.search(wordEmbeddings['fish'], 3) + console.log('Results:') + for (const result of fishResultsAfterDeletion) { + const word = + Object.entries(ids).find(([_, id]) => id === result.id)?.[0] || 'unknown' + console.log( + `- ${word} (score: ${result.score.toFixed(4)}, metadata:`, + result.metadata, + ')' + ) + } + + console.log('\nExample completed successfully!') +} + +// Check if we're in a browser or Node.js environment +if (typeof window !== 'undefined') { + // Browser environment + document.addEventListener('DOMContentLoaded', () => { + const button = document.createElement('button') + button.textContent = 'Run BrainyData Example' + button.addEventListener('click', async () => { + const output = document.createElement('pre') + document.body.appendChild(output) + + // Redirect console.log to the output element + const originalLog = console.log + console.log = (...args) => { + originalLog(...args) + output.textContent += + args + .map((arg) => + typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg + ) + .join(' ') + '\n' + } + + try { + await runExample() + } catch (error) { + console.error('Error running example:', error) + } + + // Restore console.log + console.log = originalLog + }) + + document.body.appendChild(button) + }) +} else { + // Node.js environment + runExample().catch((error) => { + console.error('Error running example:', error) + }) +} diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..5b172476 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,15 @@ +/** + * Global type declarations for Brainy + */ + +// Extend the globalThis interface to include the __ENV__ property +declare global { + var __ENV__: { + isBrowser: boolean; + isNode: string | false; + isServerless: boolean; + }; +} + +// This export is needed to make this file a module +export {}; diff --git a/src/hnsw/hnswIndex.ts b/src/hnsw/hnswIndex.ts new file mode 100644 index 00000000..ed8adf3f --- /dev/null +++ b/src/hnsw/hnswIndex.ts @@ -0,0 +1,704 @@ +/** + * HNSW (Hierarchical Navigable Small World) Index implementation + * Based on the paper: "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs" + */ + +import { DistanceFunction, HNSWConfig, HNSWNoun, Vector, VectorDocument } from '../coreTypes.js' +import { euclideanDistance } from '../utils/index.js' +import { executeInThread } from '../utils/workerUtils.js' + +// Default HNSW parameters +const DEFAULT_CONFIG: HNSWConfig = { + M: 16, // Max number of connections per noun + efConstruction: 200, // Size of a dynamic candidate list during construction + efSearch: 50, // Size of a dynamic candidate list during search + ml: 16 // Max level +} + +export class HNSWIndex { + private nouns: Map = new Map() + private entryPointId: string | null = null + private maxLevel = 0 + private config: HNSWConfig + private distanceFunction: DistanceFunction + private dimension: number | null = null + private useParallelization: boolean = true // Whether to use parallelization for performance-critical operations + + constructor( + config: Partial = {}, + distanceFunction: DistanceFunction = euclideanDistance, + options: { useParallelization?: boolean } = {} + ) { + this.config = { ...DEFAULT_CONFIG, ...config } + this.distanceFunction = distanceFunction + this.useParallelization = options.useParallelization !== undefined ? options.useParallelization : true + } + + /** + * Set whether to use parallelization for performance-critical operations + */ + public setUseParallelization(useParallelization: boolean): void { + this.useParallelization = useParallelization + } + + /** + * Get whether parallelization is enabled + */ + public getUseParallelization(): boolean { + return this.useParallelization + } + + /** + * Calculate distances between a query vector and multiple vectors in parallel + * This is used to optimize performance for search operations + * @param queryVector The query vector + * @param vectors Array of vectors to compare against + * @returns Array of distances + */ + private async calculateDistancesInParallel( + queryVector: Vector, + vectors: Array<{ id: string; vector: Vector }> + ): Promise> { + // If parallelization is disabled or there are very few vectors, use sequential processing + if (!this.useParallelization || vectors.length < 10) { + return vectors.map(item => ({ + id: item.id, + distance: this.distanceFunction(queryVector, item.vector) + })) + } + + // Function to be executed in a worker thread + const distanceCalculator = ( + args: { + queryVector: Vector, + vectors: Array<{ id: string; vector: Vector }>, + distanceFnString: string + } + ) => { + const { queryVector, vectors, distanceFnString } = args; + + // Recreate the distance function from its string representation + const distanceFunction = new Function('return ' + distanceFnString)() as DistanceFunction + + // Calculate distances for all items + return vectors.map(item => ({ + id: item.id, + distance: distanceFunction(queryVector, item.vector) + })) + } + + try { + // Convert the distance function to a string for serialization + const distanceFnString = this.distanceFunction.toString() + + // Execute the distance calculation in a separate thread + return await executeInThread>( + distanceCalculator.toString(), + { queryVector, vectors, distanceFnString } + ) + } catch (error) { + console.error('Error in parallel distance calculation, falling back to sequential:', error) + // Fall back to sequential processing if parallel execution fails + return vectors.map(item => ({ + id: item.id, + distance: this.distanceFunction(queryVector, item.vector) + })) + } + } + + /** + * Add a vector to the index + */ + public async addItem(item: VectorDocument): Promise { + // Check if item is defined + if (!item) { + throw new Error('Item is undefined or null') + } + + const { id, vector } = item + + // Check if vector is defined + if (!vector) { + throw new Error('Vector is undefined or null') + } + + // Set dimension on first insert + if (this.dimension === null) { + this.dimension = vector.length + } else if (vector.length !== this.dimension) { + throw new Error( + `Vector dimension mismatch: expected ${this.dimension}, got ${vector.length}` + ) + } + + // Generate random level for this noun + const nounLevel = this.getRandomLevel() + + // Create new noun + const noun: HNSWNoun = { + id, + vector, + connections: new Map() + } + + // Initialize empty connection sets for each level + for (let level = 0; level <= nounLevel; level++) { + noun.connections.set(level, new Set()) + } + + // If this is the first noun, make it the entry point + if (this.nouns.size === 0) { + this.entryPointId = id + this.maxLevel = nounLevel + this.nouns.set(id, noun) + return id + } + + // Find entry point + if (!this.entryPointId) { + console.error('Entry point ID is null') + // If there's no entry point, this is the first noun, so we should have returned earlier + // This is a safety check + this.entryPointId = id + this.maxLevel = nounLevel + this.nouns.set(id, noun) + return id + } + + const entryPoint = this.nouns.get(this.entryPointId) + if (!entryPoint) { + console.error(`Entry point with ID ${this.entryPointId} not found`) + // If the entry point doesn't exist, treat this as the first noun + this.entryPointId = id + this.maxLevel = nounLevel + this.nouns.set(id, noun) + return id + } + + let currObj = entryPoint + let currDist = this.distanceFunction(vector, entryPoint.vector) + + // Traverse the graph from top to bottom to find the closest noun + for (let level = this.maxLevel; level > nounLevel; level--) { + let changed = true + while (changed) { + changed = false + + // Check all neighbors at current level + const connections = currObj.connections.get(level) || new Set() + + for (const neighborId of connections) { + const neighbor = this.nouns.get(neighborId) + if (!neighbor) { + // Skip neighbors that don't exist (expected during rapid additions/deletions) + continue + } + const distToNeighbor = this.distanceFunction(vector, neighbor.vector) + + if (distToNeighbor < currDist) { + currDist = distToNeighbor + currObj = neighbor + changed = true + } + } + } + } + + // For each level from nounLevel down to 0 + for (let level = Math.min(nounLevel, this.maxLevel); level >= 0; level--) { + // Find ef nearest elements using greedy search + const nearestNouns = await this.searchLayer( + vector, + currObj, + this.config.efConstruction, + level + ) + + // Select M nearest neighbors + const neighbors = this.selectNeighbors( + vector, + nearestNouns, + this.config.M + ) + + // Add bidirectional connections + for (const [neighborId, _] of neighbors) { + const neighbor = this.nouns.get(neighborId) + if (!neighbor) { + // Skip neighbors that don't exist (expected during rapid additions/deletions) + continue + } + + noun.connections.get(level)!.add(neighborId) + + // Add reverse connection + if (!neighbor.connections.has(level)) { + neighbor.connections.set(level, new Set()) + } + neighbor.connections.get(level)!.add(id) + + // Ensure neighbor doesn't have too many connections + if (neighbor.connections.get(level)!.size > this.config.M) { + this.pruneConnections(neighbor, level) + } + } + + // Update entry point for the next level + if (nearestNouns.size > 0) { + const [nearestId, nearestDist] = [...nearestNouns][0] + if (nearestDist < currDist) { + currDist = nearestDist + const nearestNoun = this.nouns.get(nearestId) + if (!nearestNoun) { + console.error(`Nearest noun with ID ${nearestId} not found in addItem`) + // Keep the current object as is + } else { + currObj = nearestNoun + } + } + } + } + + // Update max level and entry point if needed + if (nounLevel > this.maxLevel) { + this.maxLevel = nounLevel + this.entryPointId = id + } + + // Add noun to the index + this.nouns.set(id, noun) + return id + } + + /** + * Search for nearest neighbors + */ + public async search(queryVector: Vector, k: number = 10): Promise> { + if (this.nouns.size === 0) { + return [] + } + + // Check if query vector is defined + if (!queryVector) { + throw new Error('Query vector is undefined or null') + } + + if (this.dimension !== null && queryVector.length !== this.dimension) { + throw new Error( + `Query vector dimension mismatch: expected ${this.dimension}, got ${queryVector.length}` + ) + } + + // Start from the entry point + if (!this.entryPointId) { + console.error('Entry point ID is null') + return [] + } + + const entryPoint = this.nouns.get(this.entryPointId) + if (!entryPoint) { + console.error(`Entry point with ID ${this.entryPointId} not found`) + return [] + } + + let currObj = entryPoint + let currDist = this.distanceFunction(queryVector, currObj.vector) + + // Traverse the graph from top to bottom to find the closest noun + for (let level = this.maxLevel; level > 0; level--) { + let changed = true + while (changed) { + changed = false + + // Check all neighbors at current level + const connections = currObj.connections.get(level) || new Set() + + // If we have enough connections, use parallel distance calculation + if (this.useParallelization && connections.size >= 10) { + // Prepare vectors for parallel calculation + const vectors: Array<{ id: string; vector: Vector }> = [] + for (const neighborId of connections) { + const neighbor = this.nouns.get(neighborId) + if (!neighbor) continue + vectors.push({ id: neighborId, vector: neighbor.vector }) + } + + // Calculate distances in parallel + const distances = await this.calculateDistancesInParallel(queryVector, vectors) + + // Find the closest neighbor + for (const { id, distance } of distances) { + if (distance < currDist) { + currDist = distance + const neighbor = this.nouns.get(id) + if (neighbor) { + currObj = neighbor + changed = true + } + } + } + } else { + // Use sequential processing for small number of connections + for (const neighborId of connections) { + const neighbor = this.nouns.get(neighborId) + if (!neighbor) { + // Skip neighbors that don't exist (expected during rapid additions/deletions) + continue + } + const distToNeighbor = this.distanceFunction( + queryVector, + neighbor.vector + ) + + if (distToNeighbor < currDist) { + currDist = distToNeighbor + currObj = neighbor + changed = true + } + } + } + } + } + + // Search at level 0 with ef = k + const nearestNouns = await this.searchLayer( + queryVector, + currObj, + Math.max(this.config.efSearch, k), + 0 + ) + + // Convert to array and sort by distance + return [...nearestNouns].slice(0, k) + } + + /** + * Remove an item from the index + */ + public removeItem(id: string): boolean { + if (!this.nouns.has(id)) { + return false + } + + const noun = this.nouns.get(id)! + + // Remove connections to this noun from all neighbors + for (const [level, connections] of noun.connections.entries()) { + for (const neighborId of connections) { + const neighbor = this.nouns.get(neighborId) + if (!neighbor) { + // Skip neighbors that don't exist (expected during rapid additions/deletions) + continue + } + if (neighbor.connections.has(level)) { + neighbor.connections.get(level)!.delete(id) + + // Prune connections after removing this noun to ensure consistency + this.pruneConnections(neighbor, level) + } + } + } + + // Also check all other nouns for references to this noun and remove them + for (const [nounId, otherNoun] of this.nouns.entries()) { + if (nounId === id) continue // Skip the noun being removed + + for (const [level, connections] of otherNoun.connections.entries()) { + if (connections.has(id)) { + connections.delete(id) + + // Prune connections after removing this reference + this.pruneConnections(otherNoun, level) + } + } + } + + // Remove the noun + this.nouns.delete(id) + + // If we removed the entry point, find a new one + if (this.entryPointId === id) { + if (this.nouns.size === 0) { + this.entryPointId = null + this.maxLevel = 0 + } else { + // Find the noun with the highest level + let maxLevel = 0 + let newEntryPointId = null + + for (const [nounId, noun] of this.nouns.entries()) { + if (noun.connections.size === 0) continue // Skip nouns with no connections + + const nounLevel = Math.max(...noun.connections.keys()) + if (nounLevel >= maxLevel) { + maxLevel = nounLevel + newEntryPointId = nounId + } + } + + this.entryPointId = newEntryPointId + this.maxLevel = maxLevel + } + } + + return true + } + + /** + * Get all nouns in the index + */ + public getNouns(): Map { + return new Map(this.nouns) + } + + + /** + * Clear the index + */ + public clear(): void { + this.nouns.clear() + this.entryPointId = null + this.maxLevel = 0 + } + + /** + * Get the size of the index + */ + public size(): number { + return this.nouns.size + } + + /** + * Get the distance function used by the index + */ + public getDistanceFunction(): DistanceFunction { + return this.distanceFunction + } + + /** + * Get the entry point ID + */ + public getEntryPointId(): string | null { + return this.entryPointId + } + + /** + * Get the maximum level + */ + public getMaxLevel(): number { + return this.maxLevel + } + + /** + * Get the dimension + */ + public getDimension(): number | null { + return this.dimension + } + + /** + * Get the configuration + */ + public getConfig(): HNSWConfig { + return { ...this.config } + } + + /** + * Search within a specific layer + * Returns a map of noun IDs to distances, sorted by distance + */ + private async searchLayer( + queryVector: Vector, + entryPoint: HNSWNoun, + ef: number, + level: number + ): Promise> { + // Set of visited nouns + const visited = new Set([entryPoint.id]) + + // Priority queue of candidates (closest first) + const candidates = new Map() + candidates.set( + entryPoint.id, + this.distanceFunction(queryVector, entryPoint.vector) + ) + + // Priority queue of nearest neighbors found so far (closest first) + const nearest = new Map() + nearest.set( + entryPoint.id, + this.distanceFunction(queryVector, entryPoint.vector) + ) + + // While there are candidates to explore + while (candidates.size > 0) { + // Get closest candidate + const [closestId, closestDist] = [...candidates][0] + candidates.delete(closestId) + + // If this candidate is farther than the farthest in our result set, we're done + const farthestInNearest = [...nearest][nearest.size - 1] + if (nearest.size >= ef && closestDist > farthestInNearest[1]) { + break + } + + // Explore neighbors of the closest candidate + const noun = this.nouns.get(closestId) + if (!noun) { + console.error(`Noun with ID ${closestId} not found in searchLayer`) + continue + } + const connections = noun.connections.get(level) || new Set() + + // If we have enough connections and parallelization is enabled, use parallel distance calculation + if (this.useParallelization && connections.size >= 10) { + // Collect unvisited neighbors + const unvisitedNeighbors: Array<{ id: string; vector: Vector }> = [] + for (const neighborId of connections) { + if (!visited.has(neighborId)) { + visited.add(neighborId) + const neighbor = this.nouns.get(neighborId) + if (!neighbor) continue + unvisitedNeighbors.push({ id: neighborId, vector: neighbor.vector }) + } + } + + if (unvisitedNeighbors.length > 0) { + // Calculate distances in parallel + const distances = await this.calculateDistancesInParallel(queryVector, unvisitedNeighbors) + + // Process the results + for (const { id, distance } of distances) { + // If we haven't found ef nearest neighbors yet, or this neighbor is closer than the farthest one we've found + if (nearest.size < ef || distance < farthestInNearest[1]) { + candidates.set(id, distance) + nearest.set(id, distance) + + // If we have more than ef neighbors, remove the farthest one + if (nearest.size > ef) { + const sortedNearest = [...nearest].sort((a, b) => a[1] - b[1]) + nearest.clear() + for (let i = 0; i < ef; i++) { + nearest.set(sortedNearest[i][0], sortedNearest[i][1]) + } + } + } + } + } + } else { + // Use sequential processing for small number of connections + for (const neighborId of connections) { + if (!visited.has(neighborId)) { + visited.add(neighborId) + + const neighbor = this.nouns.get(neighborId) + if (!neighbor) { + // Skip neighbors that don't exist (expected during rapid additions/deletions) + continue + } + const distToNeighbor = this.distanceFunction( + queryVector, + neighbor.vector + ) + + // If we haven't found ef nearest neighbors yet, or this neighbor is closer than the farthest one we've found + if (nearest.size < ef || distToNeighbor < farthestInNearest[1]) { + candidates.set(neighborId, distToNeighbor) + nearest.set(neighborId, distToNeighbor) + + // If we have more than ef neighbors, remove the farthest one + if (nearest.size > ef) { + const sortedNearest = [...nearest].sort((a, b) => a[1] - b[1]) + nearest.clear() + for (let i = 0; i < ef; i++) { + nearest.set(sortedNearest[i][0], sortedNearest[i][1]) + } + } + } + } + } + } + } + + // Sort nearest by distance + return new Map([...nearest].sort((a, b) => a[1] - b[1])) + } + + /** + * Select M nearest neighbors from the candidate set + */ + private selectNeighbors( + queryVector: Vector, + candidates: Map, + M: number + ): Map { + if (candidates.size <= M) { + return candidates + } + + // Simple heuristic: just take the M closest + const sortedCandidates = [...candidates].sort((a, b) => a[1] - b[1]) + const result = new Map() + + for (let i = 0; i < Math.min(M, sortedCandidates.length); i++) { + result.set(sortedCandidates[i][0], sortedCandidates[i][1]) + } + + return result + } + + /** + * Ensure a noun doesn't have too many connections at a given level + */ + private pruneConnections(noun: HNSWNoun, level: number): void { + const connections = noun.connections.get(level)! + if (connections.size <= this.config.M) { + return + } + + // Calculate distances to all neighbors + const distances = new Map() + const validNeighborIds = new Set() + + for (const neighborId of connections) { + const neighbor = this.nouns.get(neighborId) + if (!neighbor) { + // Skip neighbors that don't exist (expected during rapid additions/deletions) + continue + } + + // Only add valid neighbors to the distances map + distances.set( + neighborId, + this.distanceFunction(noun.vector, neighbor.vector) + ) + validNeighborIds.add(neighborId) + } + + // Only proceed if we have valid neighbors + if (distances.size === 0) { + // If no valid neighbors, clear connections at this level + noun.connections.set(level, new Set()) + return + } + + // Select M closest neighbors from valid ones + const selectedNeighbors = this.selectNeighbors( + noun.vector, + distances, + this.config.M + ) + + // Update connections with only valid neighbors + noun.connections.set(level, new Set(selectedNeighbors.keys())) + } + + /** + * Generate a random level for a new noun + * Uses the same distribution as in the original HNSW paper + */ + private getRandomLevel(): number { + const r = Math.random() + return Math.floor(-Math.log(r) * (1.0 / Math.log(this.config.M))) + } +} diff --git a/src/hnsw/hnswIndexOptimized.ts b/src/hnsw/hnswIndexOptimized.ts new file mode 100644 index 00000000..8823486d --- /dev/null +++ b/src/hnsw/hnswIndexOptimized.ts @@ -0,0 +1,586 @@ +/** + * Optimized HNSW (Hierarchical Navigable Small World) Index implementation + * Extends the base HNSW implementation with support for large datasets + * Uses product quantization for dimensionality reduction and disk-based storage when needed + */ + +import { + DistanceFunction, + HNSWConfig, + HNSWNoun, + Vector, + VectorDocument +} from '../coreTypes.js' +import { HNSWIndex } from './hnswIndex.js' +import { StorageAdapter } from '../coreTypes.js' + +// Configuration for the optimized HNSW index +export interface HNSWOptimizedConfig extends HNSWConfig { + // Memory threshold in bytes - when exceeded, will use disk-based approach + memoryThreshold?: number + + // Product quantization settings + productQuantization?: { + // Whether to use product quantization + enabled: boolean + // Number of subvectors to split the vector into + numSubvectors?: number + // Number of centroids per subvector + numCentroids?: number + } + + // Whether to use disk-based storage for the index + useDiskBasedIndex?: boolean +} + +// Default configuration for the optimized HNSW index +const DEFAULT_OPTIMIZED_CONFIG: HNSWOptimizedConfig = { + M: 16, + efConstruction: 200, + efSearch: 50, + ml: 16, + memoryThreshold: 1024 * 1024 * 1024, // 1GB default threshold + productQuantization: { + enabled: false, + numSubvectors: 16, + numCentroids: 256 + }, + useDiskBasedIndex: false +} + +/** + * Product Quantization implementation + * Reduces vector dimensionality by splitting vectors into subvectors + * and quantizing each subvector to the nearest centroid + */ +class ProductQuantizer { + private numSubvectors: number + private numCentroids: number + private centroids: Vector[][] = [] + private subvectorSize: number = 0 + private initialized: boolean = false + private dimension: number = 0 + + constructor(numSubvectors: number = 16, numCentroids: number = 256) { + this.numSubvectors = numSubvectors + this.numCentroids = numCentroids + } + + /** + * Initialize the product quantizer with training data + * @param vectors Training vectors to use for learning centroids + */ + public train(vectors: Vector[]): void { + if (vectors.length === 0) { + throw new Error('Cannot train product quantizer with empty vector set') + } + + this.dimension = vectors[0].length + this.subvectorSize = Math.ceil(this.dimension / this.numSubvectors) + + // Initialize centroids for each subvector + for (let i = 0; i < this.numSubvectors; i++) { + // Extract subvectors from training data + const subvectors: Vector[] = vectors.map((vector) => { + const start = i * this.subvectorSize + const end = Math.min(start + this.subvectorSize, this.dimension) + return vector.slice(start, end) + }) + + // Initialize centroids for this subvector using k-means++ + this.centroids[i] = this.kMeansPlusPlus(subvectors, this.numCentroids) + } + + this.initialized = true + } + + /** + * Quantize a vector using product quantization + * @param vector Vector to quantize + * @returns Array of centroid indices, one for each subvector + */ + public quantize(vector: Vector): number[] { + if (!this.initialized) { + throw new Error('Product quantizer not initialized. Call train() first.') + } + + if (vector.length !== this.dimension) { + throw new Error( + `Vector dimension mismatch: expected ${this.dimension}, got ${vector.length}` + ) + } + + const codes: number[] = [] + + // Quantize each subvector + for (let i = 0; i < this.numSubvectors; i++) { + const start = i * this.subvectorSize + const end = Math.min(start + this.subvectorSize, this.dimension) + const subvector = vector.slice(start, end) + + // Find nearest centroid + let minDist = Number.MAX_VALUE + let nearestCentroidIndex = 0 + + for (let j = 0; j < this.centroids[i].length; j++) { + const centroid = this.centroids[i][j] + const dist = this.euclideanDistanceSquared(subvector, centroid) + + if (dist < minDist) { + minDist = dist + nearestCentroidIndex = j + } + } + + codes.push(nearestCentroidIndex) + } + + return codes + } + + /** + * Reconstruct a vector from its quantized representation + * @param codes Array of centroid indices + * @returns Reconstructed vector + */ + public reconstruct(codes: number[]): Vector { + if (!this.initialized) { + throw new Error('Product quantizer not initialized. Call train() first.') + } + + if (codes.length !== this.numSubvectors) { + throw new Error( + `Code length mismatch: expected ${this.numSubvectors}, got ${codes.length}` + ) + } + + const reconstructed: Vector = [] + + // Reconstruct each subvector + for (let i = 0; i < this.numSubvectors; i++) { + const centroidIndex = codes[i] + const centroid = this.centroids[i][centroidIndex] + + // Add centroid components to reconstructed vector + for (const component of centroid) { + reconstructed.push(component) + } + } + + // Trim to original dimension if needed + return reconstructed.slice(0, this.dimension) + } + + /** + * Compute squared Euclidean distance between two vectors + * @param a First vector + * @param b Second vector + * @returns Squared Euclidean distance + */ + private euclideanDistanceSquared(a: Vector, b: Vector): number { + let sum = 0 + const length = Math.min(a.length, b.length) + + for (let i = 0; i < length; i++) { + const diff = a[i] - b[i] + sum += diff * diff + } + + return sum + } + + /** + * Implement k-means++ algorithm to initialize centroids + * @param vectors Vectors to cluster + * @param k Number of clusters + * @returns Array of centroids + */ + private kMeansPlusPlus(vectors: Vector[], k: number): Vector[] { + if (vectors.length < k) { + // If we have fewer vectors than centroids, use the vectors as centroids + return [...vectors] + } + + const centroids: Vector[] = [] + + // Choose first centroid randomly + const firstIndex = Math.floor(Math.random() * vectors.length) + centroids.push([...vectors[firstIndex]]) + + // Choose remaining centroids + for (let i = 1; i < k; i++) { + // Compute distances to nearest centroid for each vector + const distances: number[] = vectors.map((vector) => { + let minDist = Number.MAX_VALUE + + for (const centroid of centroids) { + const dist = this.euclideanDistanceSquared(vector, centroid) + minDist = Math.min(minDist, dist) + } + + return minDist + }) + + // Compute sum of distances + const distSum = distances.reduce((sum, dist) => sum + dist, 0) + + // Choose next centroid with probability proportional to distance + let r = Math.random() * distSum + let nextIndex = 0 + + for (let j = 0; j < distances.length; j++) { + r -= distances[j] + if (r <= 0) { + nextIndex = j + break + } + } + + centroids.push([...vectors[nextIndex]]) + } + + return centroids + } + + /** + * Get the centroids for each subvector + * @returns Array of centroid arrays + */ + public getCentroids(): Vector[][] { + return this.centroids + } + + /** + * Set the centroids for each subvector + * @param centroids Array of centroid arrays + */ + public setCentroids(centroids: Vector[][]): void { + this.centroids = centroids + this.numSubvectors = centroids.length + this.numCentroids = centroids[0].length + this.initialized = true + } + + /** + * Get the dimension of the vectors + * @returns Dimension + */ + public getDimension(): number { + return this.dimension + } + + /** + * Set the dimension of the vectors + * @param dimension Dimension + */ + public setDimension(dimension: number): void { + this.dimension = dimension + this.subvectorSize = Math.ceil(dimension / this.numSubvectors) + } +} + +/** + * Optimized HNSW Index implementation + * Extends the base HNSW implementation with support for large datasets + * Uses product quantization for dimensionality reduction and disk-based storage when needed + */ +export class HNSWIndexOptimized extends HNSWIndex { + private optimizedConfig: HNSWOptimizedConfig + private productQuantizer: ProductQuantizer | null = null + private storage: StorageAdapter | null = null + private useDiskBasedIndex: boolean = false + private useProductQuantization: boolean = false + private quantizedVectors: Map = new Map() + private memoryUsage: number = 0 + private vectorCount: number = 0 + + constructor( + config: Partial = {}, + distanceFunction: DistanceFunction, + storage: StorageAdapter | null = null + ) { + // Initialize base HNSW index with standard config + super(config, distanceFunction) + + // Set optimized config + this.optimizedConfig = { ...DEFAULT_OPTIMIZED_CONFIG, ...config } + + // Set storage adapter + this.storage = storage + + // Initialize product quantizer if enabled + if (this.optimizedConfig.productQuantization?.enabled) { + this.useProductQuantization = true + this.productQuantizer = new ProductQuantizer( + this.optimizedConfig.productQuantization.numSubvectors, + this.optimizedConfig.productQuantization.numCentroids + ) + } + + // Set disk-based index flag + this.useDiskBasedIndex = this.optimizedConfig.useDiskBasedIndex || false + } + + /** + * Add a vector to the index + * Uses product quantization if enabled and memory threshold is exceeded + */ + public override async addItem(item: VectorDocument): Promise { + // Check if item is defined + if (!item) { + throw new Error('Item is undefined or null') + } + + const { id, vector } = item + + // Check if vector is defined + if (!vector) { + throw new Error('Vector is undefined or null') + } + + // Estimate memory usage for this vector + const vectorMemory = vector.length * 8 // 8 bytes per number (Float64) + const connectionsMemory = this.optimizedConfig.M * this.optimizedConfig.ml * 16 // Estimate for connections + const totalMemory = vectorMemory + connectionsMemory + + // Update memory usage estimate + this.memoryUsage += totalMemory + this.vectorCount++ + + // Check if we should switch to product quantization + if ( + this.useProductQuantization && + this.memoryUsage > this.optimizedConfig.memoryThreshold! && + this.productQuantizer && + !this.productQuantizer.getDimension() + ) { + // Initialize product quantizer with existing vectors + this.initializeProductQuantizer() + } + + // If product quantization is active, quantize the vector + if ( + this.useProductQuantization && + this.productQuantizer && + this.productQuantizer.getDimension() > 0 + ) { + // Quantize the vector + const codes = this.productQuantizer.quantize(vector) + + // Store the quantized vector + this.quantizedVectors.set(id, codes) + + // Reconstruct the vector for indexing + const reconstructedVector = this.productQuantizer.reconstruct(codes) + + // Add the reconstructed vector to the index + return await super.addItem({ id, vector: reconstructedVector }) + } + + // If disk-based index is active and storage is available, store the vector + if (this.useDiskBasedIndex && this.storage) { + // Create a noun object + const noun: HNSWNoun = { + id, + vector, + connections: new Map() + } + + // Store the noun + this.storage.saveNoun(noun).catch((error) => { + console.error(`Failed to save noun ${id} to storage:`, error) + }) + } + + // Add the vector to the in-memory index + return await super.addItem(item) + } + + /** + * Search for nearest neighbors + * Uses product quantization if enabled + */ + public override async search( + queryVector: Vector, + k: number = 10 + ): Promise> { + // Check if query vector is defined + if (!queryVector) { + throw new Error('Query vector is undefined or null') + } + + // If product quantization is active, quantize the query vector + if ( + this.useProductQuantization && + this.productQuantizer && + this.productQuantizer.getDimension() > 0 + ) { + // Quantize the query vector + const codes = this.productQuantizer.quantize(queryVector) + + // Reconstruct the query vector + const reconstructedVector = this.productQuantizer.reconstruct(codes) + + // Search with the reconstructed vector + return await super.search(reconstructedVector, k) + } + + // Otherwise, use the standard search + return await super.search(queryVector, k) + } + + /** + * Remove an item from the index + */ + public override removeItem(id: string): boolean { + // If product quantization is active, remove the quantized vector + if (this.useProductQuantization) { + this.quantizedVectors.delete(id) + } + + // If disk-based index is active and storage is available, remove the vector from storage + if (this.useDiskBasedIndex && this.storage) { + this.storage.deleteNoun(id).catch((error) => { + console.error(`Failed to delete noun ${id} from storage:`, error) + }) + } + + // Update memory usage estimate + if (this.vectorCount > 0) { + this.memoryUsage = Math.max( + 0, + this.memoryUsage - this.memoryUsage / this.vectorCount + ) + this.vectorCount-- + } + + // Remove the item from the in-memory index + return super.removeItem(id) + } + + /** + * Clear the index + */ + public override clear(): void { + // Clear product quantization data + if (this.useProductQuantization) { + this.quantizedVectors.clear() + this.productQuantizer = new ProductQuantizer( + this.optimizedConfig.productQuantization!.numSubvectors, + this.optimizedConfig.productQuantization!.numCentroids + ) + } + + // Reset memory usage + this.memoryUsage = 0 + this.vectorCount = 0 + + // Clear the in-memory index + super.clear() + } + + /** + * Initialize product quantizer with existing vectors + */ + private initializeProductQuantizer(): void { + if (!this.productQuantizer) { + return + } + + // Get all vectors from the index + const nouns = super.getNouns() + const vectors: Vector[] = [] + + // Extract vectors + for (const [_, noun] of nouns) { + vectors.push(noun.vector) + } + + // Train the product quantizer + if (vectors.length > 0) { + this.productQuantizer.train(vectors) + + // Quantize all existing vectors + for (const [id, noun] of nouns) { + const codes = this.productQuantizer.quantize(noun.vector) + this.quantizedVectors.set(id, codes) + } + + console.log( + `Initialized product quantizer with ${vectors.length} vectors` + ) + } + } + + /** + * Get the product quantizer + * @returns Product quantizer or null if not enabled + */ + public getProductQuantizer(): ProductQuantizer | null { + return this.productQuantizer + } + + /** + * Get the optimized configuration + * @returns Optimized configuration + */ + public getOptimizedConfig(): HNSWOptimizedConfig { + return { ...this.optimizedConfig } + } + + /** + * Get the estimated memory usage + * @returns Estimated memory usage in bytes + */ + public getMemoryUsage(): number { + return this.memoryUsage + } + + /** + * Set the storage adapter + * @param storage Storage adapter + */ + public setStorage(storage: StorageAdapter): void { + this.storage = storage + } + + /** + * Get the storage adapter + * @returns Storage adapter or null if not set + */ + public getStorage(): StorageAdapter | null { + return this.storage + } + + /** + * Set whether to use disk-based index + * @param useDiskBasedIndex Whether to use disk-based index + */ + public setUseDiskBasedIndex(useDiskBasedIndex: boolean): void { + this.useDiskBasedIndex = useDiskBasedIndex + } + + /** + * Get whether disk-based index is used + * @returns Whether disk-based index is used + */ + public getUseDiskBasedIndex(): boolean { + return this.useDiskBasedIndex + } + + /** + * Set whether to use product quantization + * @param useProductQuantization Whether to use product quantization + */ + public setUseProductQuantization(useProductQuantization: boolean): void { + this.useProductQuantization = useProductQuantization + } + + /** + * Get whether product quantization is used + * @returns Whether product quantization is used + */ + public getUseProductQuantization(): boolean { + return this.useProductQuantization + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..a638d5f7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,351 @@ +/** + * OPFS BrainyData + * A vector database using HNSW indexing with Origin Private File System storage + */ + +// Export main BrainyData class and related types +import { BrainyData, BrainyDataConfig } from './brainyData.js' + +export { BrainyData } +export type { BrainyDataConfig } + +// Export distance functions for convenience +import { + euclideanDistance, + cosineDistance, + manhattanDistance, + dotProductDistance +} from './utils/index.js' + +export { + euclideanDistance, + cosineDistance, + manhattanDistance, + dotProductDistance +} + +// Export embedding functionality +import { + UniversalSentenceEncoder, + createEmbeddingFunction, + createTensorFlowEmbeddingFunction, + createThreadedEmbeddingFunction, + defaultEmbeddingFunction +} from './utils/embedding.js' + +export { + UniversalSentenceEncoder, + createEmbeddingFunction, + createTensorFlowEmbeddingFunction, + createThreadedEmbeddingFunction, + defaultEmbeddingFunction +} + +// Export storage adapters +import { + OPFSStorage, + MemoryStorage, + createStorage +} from './storage/opfsStorage.js' +import { FileSystemStorage } from './storage/fileSystemStorage.js' +import { R2Storage, S3CompatibleStorage } from './storage/s3CompatibleStorage.js' + +export { + OPFSStorage, + MemoryStorage, + FileSystemStorage, + R2Storage, + S3CompatibleStorage, + createStorage +} + +// Export unified pipeline +import { + Pipeline, + pipeline, + augmentationPipeline, + ExecutionMode, + PipelineOptions, + PipelineResult, + executeStreamlined, + executeByType, + executeSingle, + processStaticData, + processStreamingData, + createPipeline, + createStreamingPipeline, + StreamlinedExecutionMode, + StreamlinedPipelineOptions, + StreamlinedPipelineResult +} from './pipeline.js' + +// Export sequential pipeline (for backward compatibility) +import { + SequentialPipeline, + sequentialPipeline, + SequentialPipelineOptions +} from './sequentialPipeline.js' + +// Export augmentation factory +import { + createSenseAugmentation, + addWebSocketSupport, + executeAugmentation, + loadAugmentationModule, + AugmentationOptions +} from './augmentationFactory.js' + +export { + // Unified pipeline exports + Pipeline, + pipeline, + augmentationPipeline, + ExecutionMode, + SequentialPipeline, + sequentialPipeline, + + // Streamlined pipeline exports (now part of unified pipeline) + executeStreamlined, + executeByType, + executeSingle, + processStaticData, + processStreamingData, + createPipeline, + createStreamingPipeline, + StreamlinedExecutionMode, + + // Augmentation factory exports + createSenseAugmentation, + addWebSocketSupport, + executeAugmentation, + loadAugmentationModule +} +export type { + PipelineOptions, + PipelineResult, + SequentialPipelineOptions, + StreamlinedPipelineOptions, + StreamlinedPipelineResult, + AugmentationOptions +} + +// Export augmentation registry for build-time loading +import { + availableAugmentations, + registerAugmentation, + initializeAugmentationPipeline, + setAugmentationEnabled, + getAugmentationsByType +} from './augmentationRegistry.js' + +export { + availableAugmentations, + registerAugmentation, + initializeAugmentationPipeline, + setAugmentationEnabled, + getAugmentationsByType +} + +// Export augmentation registry loader for build tools +import { + loadAugmentationsFromModules, + createAugmentationRegistryPlugin, + createAugmentationRegistryRollupPlugin +} from './augmentationRegistryLoader.js' +import type { + AugmentationRegistryLoaderOptions, + AugmentationLoadResult +} from './augmentationRegistryLoader.js' + +export { + loadAugmentationsFromModules, + createAugmentationRegistryPlugin, + createAugmentationRegistryRollupPlugin +} +export type { + AugmentationRegistryLoaderOptions, + AugmentationLoadResult +} + +// Export augmentation implementations +import { + MemoryStorageAugmentation, + FileSystemStorageAugmentation, + OPFSStorageAugmentation, + createMemoryAugmentation +} from './augmentations/memoryAugmentations.js' +import { + WebSocketConduitAugmentation, + WebRTCConduitAugmentation, + createConduitAugmentation +} from './augmentations/conduitAugmentations.js' +import { + ServerSearchConduitAugmentation, + ServerSearchActivationAugmentation, + createServerSearchAugmentations +} from './augmentations/serverSearchAugmentations.js' + +// Non-LLM exports +export { + MemoryStorageAugmentation, + FileSystemStorageAugmentation, + OPFSStorageAugmentation, + createMemoryAugmentation, + WebSocketConduitAugmentation, + WebRTCConduitAugmentation, + createConduitAugmentation, + ServerSearchConduitAugmentation, + ServerSearchActivationAugmentation, + createServerSearchAugmentations +} + +// LLM augmentations are optional and not imported by default +// They can be imported directly from their module if needed: +// import { LLMCognitionAugmentation, LLMActivationAugmentation, createLLMAugmentations } from './augmentations/llmAugmentations.js' + + +// Export types +import type { + Vector, + VectorDocument, + SearchResult, + DistanceFunction, + EmbeddingFunction, + EmbeddingModel, + HNSWNoun, + GraphVerb, + HNSWConfig, + StorageAdapter +} from './coreTypes.js' + +// Export HNSW index and optimized version +import { HNSWIndex } from './hnsw/hnswIndex.js' +import { HNSWIndexOptimized, HNSWOptimizedConfig } from './hnsw/hnswIndexOptimized.js' + +export { + HNSWIndex, + HNSWIndexOptimized +} + +export type { + Vector, + VectorDocument, + SearchResult, + DistanceFunction, + EmbeddingFunction, + EmbeddingModel, + HNSWNoun, + GraphVerb, + HNSWConfig, + HNSWOptimizedConfig, + StorageAdapter +} + +// Export augmentation types +import type { + IAugmentation, + AugmentationResponse, + IWebSocketSupport, + ISenseAugmentation, + IConduitAugmentation, + ICognitionAugmentation, + IMemoryAugmentation, + IPerceptionAugmentation, + IDialogAugmentation, + IActivationAugmentation +} from './types/augmentations.js' +import { AugmentationType, BrainyAugmentations } from './types/augmentations.js' + +export type { + IAugmentation, + AugmentationResponse, + IWebSocketSupport +} +export { + AugmentationType, + BrainyAugmentations, + ISenseAugmentation, + IConduitAugmentation, + ICognitionAugmentation, + IMemoryAugmentation, + IPerceptionAugmentation, + IDialogAugmentation, + IActivationAugmentation +} + +// Export combined WebSocket augmentation interfaces +export type { + IWebSocketCognitionAugmentation, + IWebSocketSenseAugmentation, + IWebSocketPerceptionAugmentation, + IWebSocketActivationAugmentation, + IWebSocketDialogAugmentation, + IWebSocketConduitAugmentation, + IWebSocketMemoryAugmentation +} from './types/augmentations.js' + +// Export graph types +import type { + GraphNoun, + EmbeddedGraphVerb, + Person, + Place, + Thing, + Event, + Concept, + Content +} from './types/graphTypes.js' +import { NounType, VerbType } from './types/graphTypes.js' + +export type { + GraphNoun, + EmbeddedGraphVerb, + Person, + Place, + Thing, + Event, + Concept, + Content +} +export { NounType, VerbType } + +// Export MCP (Model Control Protocol) components +import { + BrainyMCPAdapter, + MCPAugmentationToolset, + BrainyMCPService +} from './mcp/index.js' // Import from mcp/index.js +import { + MCPRequest, + MCPResponse, + MCPDataAccessRequest, + MCPToolExecutionRequest, + MCPSystemInfoRequest, + MCPAuthenticationRequest, + MCPRequestType, + MCPServiceOptions, + MCPTool, + MCP_VERSION +} from './types/mcpTypes.js' + +export { + // MCP classes + BrainyMCPAdapter, + MCPAugmentationToolset, + BrainyMCPService, + + // MCP types + MCPRequestType, + MCP_VERSION +} + +export type { + MCPRequest, + MCPResponse, + MCPDataAccessRequest, + MCPToolExecutionRequest, + MCPSystemInfoRequest, + MCPAuthenticationRequest, + MCPServiceOptions, + MCPTool +} diff --git a/src/mcp/README.md b/src/mcp/README.md new file mode 100644 index 00000000..051fa368 --- /dev/null +++ b/src/mcp/README.md @@ -0,0 +1,180 @@ +# Model Control Protocol (MCP) for Brainy + +This document provides information about the Model Control Protocol (MCP) implementation in Brainy, which allows external models to access Brainy data and use the augmentation pipeline as tools. + +## Components + +The MCP implementation consists of three main components: + +1. **BrainyMCPAdapter**: Provides access to Brainy data through MCP +2. **MCPAugmentationToolset**: Exposes the augmentation pipeline as tools +3. **BrainyMCPService**: Integrates the adapter and toolset, providing WebSocket and REST server implementations for external model access + +## Environment Compatibility + +### BrainyMCPAdapter + +The `BrainyMCPAdapter` has no environment-specific dependencies and can run in any environment where Brainy itself runs, including: + +- Browser environments +- Node.js environments +- Server environments + +### MCPAugmentationToolset + +The `MCPAugmentationToolset` also has no environment-specific dependencies and can run in any environment where Brainy itself runs, including: + +- Browser environments +- Node.js environments +- Server environments + +### BrainyMCPService + +The `BrainyMCPService` has been refactored to separate the core functionality from the Node.js-specific server functionality: + +1. **Core Functionality**: The core request handling functionality (`handleMCPRequest`) can run in any environment where Brainy itself runs. This is what remains in the main Brainy package. + +2. **Server Functionality**: The WebSocket and REST server functionality has been moved to the cloud-wrapper project to avoid including Node.js-specific dependencies in the browser bundle: + - `ws` for WebSocket server + - `express` for REST API + - `cors` for Cross-Origin Resource Sharing + +This separation ensures that the browser bundle remains lightweight and doesn't include unnecessary Node.js-specific dependencies. In browser or other environments, you can still use the core functionality through the `handleMCPRequest` method. + +## Usage + +### In Any Environment (Browser, Node.js, Server) + +```typescript +import { BrainyData, BrainyMCPAdapter, MCPAugmentationToolset } from '@soulcraft/brainy' + +// Create a BrainyData instance +const brainyData = new BrainyData() +await brainyData.init() + +// Create an MCP adapter +const adapter = new BrainyMCPAdapter(brainyData) + +// Create a toolset +const toolset = new MCPAugmentationToolset() + +// Use the adapter to access Brainy data +const response = await adapter.handleRequest({ + type: 'data_access', + operation: 'search', + requestId: adapter.generateRequestId(), + version: '1.0.0', + parameters: { + query: 'example query', + k: 5 + } +}) + +// Use the toolset to execute augmentation pipeline tools +const toolResponse = await toolset.handleRequest({ + type: 'tool_execution', + toolName: 'brainy_memory_storeData', + requestId: toolset.generateRequestId(), + version: '1.0.0', + parameters: { + args: ['key1', { some: 'data' }] + } +}) +``` + +### In Node.js Environment with Server Functionality + +To use the MCP service with WebSocket and REST server functionality, you should use the cloud-wrapper project: + +```typescript +import { BrainyData } from '@soulcraft/brainy' +import { initializeBrainy } from './services/brainyService.js' +import { initializeMCPService } from './services/mcpService.js' + +// Initialize Brainy +const brainyData = await initializeBrainy() + +// Initialize MCP service with WebSocket and REST server functionality +const mcpService = initializeMCPService(brainyData, { + wsPort: 8080, + restPort: 3000, + enableAuth: true, + apiKeys: ['your-api-key'], + rateLimit: { + maxRequests: 100, + windowMs: 60000 // 1 minute + }, + cors: { + origin: '*', + credentials: true + } +}) +``` + +Alternatively, you can configure the MCP service using environment variables in the cloud-wrapper: + +``` +# MCP configuration +MCP_WS_PORT=8080 +MCP_REST_PORT=3000 +MCP_ENABLE_AUTH=true +MCP_API_KEYS=your-api-key,another-key +MCP_RATE_LIMIT_REQUESTS=100 +MCP_RATE_LIMIT_WINDOW_MS=60000 +MCP_ENABLE_CORS=true +``` + +### In Browser Environment (Core Functionality Only) + +```typescript +import { BrainyData, BrainyMCPService } from '@soulcraft/brainy' + +// Create a BrainyData instance +const brainyData = new BrainyData() +await brainyData.init() + +// Create an MCP service (server functionality will be disabled in browser) +const mcpService = new BrainyMCPService(brainyData) + +// Use the core functionality +const response = await mcpService.handleMCPRequest({ + type: 'data_access', + operation: 'search', + requestId: mcpService.generateRequestId(), + version: '1.0.0', + parameters: { + query: 'example query', + k: 5 + } +}) +``` + +## Cloud Wrapper Integration + +The MCP service's server functionality has been integrated directly into the cloud-wrapper project. The cloud-wrapper automatically initializes the MCP service if the appropriate environment variables are set: + +``` +# MCP configuration +MCP_WS_PORT=8080 +MCP_REST_PORT=3000 +MCP_ENABLE_AUTH=true +MCP_API_KEYS=your-api-key,another-key +MCP_RATE_LIMIT_REQUESTS=100 +MCP_RATE_LIMIT_WINDOW_MS=60000 +MCP_ENABLE_CORS=true +``` + +You can deploy the cloud wrapper to various cloud platforms using the npm scripts from the root directory: + +```bash +# Deploy to AWS Lambda and API Gateway +npm run deploy:cloud:aws + +# Deploy to Google Cloud Run +npm run deploy:cloud:gcp + +# Deploy to Cloudflare Workers +npm run deploy:cloud:cloudflare +``` + +The cloud wrapper is specifically designed for server environments and includes additional features like logging, security headers, and deployment scripts for various cloud providers. See the [Cloud Wrapper README](../../cloud-wrapper/README.md) for detailed configuration instructions and API documentation. diff --git a/src/mcp/brainyMCPAdapter.ts b/src/mcp/brainyMCPAdapter.ts new file mode 100644 index 00000000..1ffcd853 --- /dev/null +++ b/src/mcp/brainyMCPAdapter.ts @@ -0,0 +1,203 @@ +/** + * BrainyMCPAdapter + * + * This class provides an adapter for accessing Brainy data through the Model Control Protocol (MCP). + * It wraps a BrainyData instance and exposes methods for getting vectors, searching similar items, + * and getting relationships. + */ + +import { v4 as uuidv4 } from 'uuid' +import { BrainyDataInterface } from '../types/brainyDataInterface.js' +import { + MCPRequest, + MCPResponse, + MCPDataAccessRequest, + MCPRequestType, + MCP_VERSION +} from '../types/mcpTypes.js' + +export class BrainyMCPAdapter { + private brainyData: BrainyDataInterface + + /** + * Creates a new BrainyMCPAdapter + * @param brainyData The BrainyData instance to wrap + */ + constructor(brainyData: BrainyDataInterface) { + this.brainyData = brainyData + } + + /** + * Handles an MCP data access request + * @param request The MCP request + * @returns An MCP response + */ + async handleRequest(request: MCPDataAccessRequest): Promise { + try { + switch (request.operation) { + case 'get': + return await this.handleGetRequest(request) + case 'search': + return await this.handleSearchRequest(request) + case 'add': + return await this.handleAddRequest(request) + case 'getRelationships': + return await this.handleGetRelationshipsRequest(request) + default: + return this.createErrorResponse( + request.requestId, + 'UNSUPPORTED_OPERATION', + `Operation ${request.operation} is not supported` + ) + } + } catch (error) { + return this.createErrorResponse( + request.requestId, + 'INTERNAL_ERROR', + error instanceof Error ? error.message : String(error) + ) + } + } + + /** + * Handles a get request + * @param request The MCP request + * @returns An MCP response + */ + private async handleGetRequest(request: MCPDataAccessRequest): Promise { + const { id } = request.parameters + + if (!id) { + return this.createErrorResponse( + request.requestId, + 'MISSING_PARAMETER', + 'Parameter "id" is required' + ) + } + + const noun = await this.brainyData.get(id) + + if (!noun) { + return this.createErrorResponse( + request.requestId, + 'NOT_FOUND', + `No noun found with id ${id}` + ) + } + + return this.createSuccessResponse(request.requestId, noun) + } + + /** + * Handles a search request + * @param request The MCP request + * @returns An MCP response + */ + private async handleSearchRequest(request: MCPDataAccessRequest): Promise { + const { query, k = 10 } = request.parameters + + if (!query) { + return this.createErrorResponse( + request.requestId, + 'MISSING_PARAMETER', + 'Parameter "query" is required' + ) + } + + const results = await this.brainyData.searchText(query, k) + return this.createSuccessResponse(request.requestId, results) + } + + /** + * Handles an add request + * @param request The MCP request + * @returns An MCP response + */ + private async handleAddRequest(request: MCPDataAccessRequest): Promise { + const { text, metadata } = request.parameters + + if (!text) { + return this.createErrorResponse( + request.requestId, + 'MISSING_PARAMETER', + 'Parameter "text" is required' + ) + } + + const id = await this.brainyData.add(text, metadata) + return this.createSuccessResponse(request.requestId, { id }) + } + + /** + * Handles a getRelationships request + * @param request The MCP request + * @returns An MCP response + */ + private async handleGetRelationshipsRequest(request: MCPDataAccessRequest): Promise { + const { id } = request.parameters + + if (!id) { + return this.createErrorResponse( + request.requestId, + 'MISSING_PARAMETER', + 'Parameter "id" is required' + ) + } + + // This is a simplified implementation - in a real implementation, we would + // need to check if these methods exist on the BrainyDataInterface + const outgoing = await (this.brainyData as any).getVerbsBySource?.(id) || [] + const incoming = await (this.brainyData as any).getVerbsByTarget?.(id) || [] + + return this.createSuccessResponse(request.requestId, { outgoing, incoming }) + } + + /** + * Creates a success response + * @param requestId The request ID + * @param data The response data + * @returns An MCP response + */ + private createSuccessResponse(requestId: string, data: any): MCPResponse { + return { + success: true, + requestId, + version: MCP_VERSION, + data + } + } + + /** + * Creates an error response + * @param requestId The request ID + * @param code The error code + * @param message The error message + * @param details Optional error details + * @returns An MCP response + */ + private createErrorResponse( + requestId: string, + code: string, + message: string, + details?: any + ): MCPResponse { + return { + success: false, + requestId, + version: MCP_VERSION, + error: { + code, + message, + details + } + } + } + + /** + * Creates a new request ID + * @returns A new UUID + */ + generateRequestId(): string { + return uuidv4() + } +} diff --git a/src/mcp/brainyMCPService.ts b/src/mcp/brainyMCPService.ts new file mode 100644 index 00000000..a68a3955 --- /dev/null +++ b/src/mcp/brainyMCPService.ts @@ -0,0 +1,347 @@ +/** + * BrainyMCPService + * + * This class provides a unified service for accessing Brainy data and augmentations + * through the Model Control Protocol (MCP). It integrates the BrainyMCPAdapter and + * MCPAugmentationToolset classes and provides WebSocket and REST server implementations + * for external model access. + */ + +import { v4 as uuidv4 } from 'uuid' +import { BrainyDataInterface } from '../types/brainyDataInterface.js' +import { + MCPRequest, + MCPResponse, + MCPDataAccessRequest, + MCPToolExecutionRequest, + MCPSystemInfoRequest, + MCPAuthenticationRequest, + MCPRequestType, + MCPServiceOptions, + MCP_VERSION, + MCPTool +} from '../types/mcpTypes.js' +import { BrainyMCPAdapter } from './brainyMCPAdapter.js' +import { MCPAugmentationToolset } from './mcpAugmentationToolset.js' +import { isBrowser, isNode } from '../utils/environment.js' + +export class BrainyMCPService { + private dataAdapter: BrainyMCPAdapter + private toolset: MCPAugmentationToolset + private options: MCPServiceOptions + private authTokens: Map + private rateLimits: Map + + /** + * Creates a new BrainyMCPService + * @param brainyData The BrainyData instance to wrap + * @param options Configuration options for the service + */ + constructor( + brainyData: BrainyDataInterface, + options: MCPServiceOptions = {} + ) { + this.dataAdapter = new BrainyMCPAdapter(brainyData) + this.toolset = new MCPAugmentationToolset() + this.options = options + this.authTokens = new Map() + this.rateLimits = new Map() + } + + /** + * Handles an MCP request + * @param request The MCP request + * @returns An MCP response + */ + async handleRequest(request: MCPRequest): Promise { + try { + switch (request.type) { + case MCPRequestType.DATA_ACCESS: + return await this.dataAdapter.handleRequest( + request as MCPDataAccessRequest + ) + + case MCPRequestType.TOOL_EXECUTION: + return await this.toolset.handleRequest( + request as MCPToolExecutionRequest + ) + + case MCPRequestType.SYSTEM_INFO: + return await this.handleSystemInfoRequest( + request as MCPSystemInfoRequest + ) + + case MCPRequestType.AUTHENTICATION: + return await this.handleAuthenticationRequest( + request as MCPAuthenticationRequest + ) + + default: + return this.createErrorResponse( + request.requestId, + 'UNSUPPORTED_REQUEST_TYPE', + `Request type ${request.type} is not supported` + ) + } + } catch (error) { + return this.createErrorResponse( + request.requestId, + 'INTERNAL_ERROR', + error instanceof Error ? error.message : String(error) + ) + } + } + + /** + * Handles a system info request + * @param request The MCP request + * @returns An MCP response + */ + private async handleSystemInfoRequest( + request: MCPSystemInfoRequest + ): Promise { + try { + switch (request.infoType) { + case 'status': + return this.createSuccessResponse(request.requestId, { + status: 'active', + version: MCP_VERSION, + environment: isBrowser() ? 'browser' : isNode() ? 'node' : 'unknown' + }) + + case 'availableTools': + const tools: MCPTool[] = await this.toolset.getAvailableTools() + return this.createSuccessResponse(request.requestId, tools) + + case 'version': + return this.createSuccessResponse(request.requestId, { + version: MCP_VERSION + }) + + default: + return this.createErrorResponse( + request.requestId, + 'UNSUPPORTED_INFO_TYPE', + `Info type ${request.infoType} is not supported` + ) + } + } catch (error) { + return this.createErrorResponse( + request.requestId, + 'INTERNAL_ERROR', + error instanceof Error ? error.message : String(error) + ) + } + } + + /** + * Handles an authentication request + * @param request The MCP request + * @returns An MCP response + */ + private async handleAuthenticationRequest( + request: MCPAuthenticationRequest + ): Promise { + try { + if (!this.options.enableAuth) { + return this.createSuccessResponse(request.requestId, { + authenticated: true, + message: 'Authentication is not enabled' + }) + } + + const { credentials } = request + + // Check API key authentication + if ( + credentials.apiKey && + this.options.apiKeys?.includes(credentials.apiKey) + ) { + const token = this.generateAuthToken('api-user') + return this.createSuccessResponse(request.requestId, { + authenticated: true, + token + }) + } + + // Check username/password authentication + // This is a placeholder - in a real implementation, you would check against a database + if ( + credentials.username === 'admin' && + credentials.password === 'password' + ) { + const token = this.generateAuthToken(credentials.username) + return this.createSuccessResponse(request.requestId, { + authenticated: true, + token + }) + } + + return this.createErrorResponse( + request.requestId, + 'INVALID_CREDENTIALS', + 'Invalid credentials' + ) + } catch (error) { + return this.createErrorResponse( + request.requestId, + 'INTERNAL_ERROR', + error instanceof Error ? error.message : String(error) + ) + } + } + + /** + * Checks if a request is valid + * @param request The request to check + * @returns Whether the request is valid + */ + private isValidRequest(request: any): boolean { + return ( + request && + typeof request === 'object' && + request.type && + request.requestId && + request.version + ) + } + + /** + * Checks if a request is authenticated + * @param request The request to check + * @returns Whether the request is authenticated + */ + private isAuthenticated(request: MCPRequest): boolean { + if (!this.options.enableAuth) { + return true + } + + return request.authToken ? this.isValidToken(request.authToken) : false + } + + /** + * Checks if a token is valid + * @param token The token to check + * @returns Whether the token is valid + */ + private isValidToken(token: string): boolean { + const tokenInfo = this.authTokens.get(token) + if (!tokenInfo) { + return false + } + + if (tokenInfo.expires < Date.now()) { + this.authTokens.delete(token) + return false + } + + return true + } + + /** + * Generates an authentication token + * @param userId The user ID to associate with the token + * @returns The generated token + */ + private generateAuthToken(userId: string): string { + const token = uuidv4() + const expires = Date.now() + 24 * 60 * 60 * 1000 // 24 hours + + this.authTokens.set(token, { userId, expires }) + + return token + } + + /** + * Checks if a client has exceeded the rate limit + * @param clientId The client ID to check + * @returns Whether the client is within the rate limit + */ + private checkRateLimit(clientId: string): boolean { + if (!this.options.rateLimit) { + return true + } + + const now = Date.now() + const limit = this.rateLimits.get(clientId) + + if (!limit) { + this.rateLimits.set(clientId, { + count: 1, + resetTime: now + this.options.rateLimit.windowMs + }) + return true + } + + if (limit.resetTime < now) { + limit.count = 1 + limit.resetTime = now + this.options.rateLimit.windowMs + return true + } + + if (limit.count >= this.options.rateLimit.maxRequests) { + return false + } + + limit.count++ + return true + } + + /** + * Creates a success response + * @param requestId The request ID + * @param data The response data + * @returns An MCP response + */ + private createSuccessResponse(requestId: string, data: any): MCPResponse { + return { + success: true, + requestId, + version: MCP_VERSION, + data + } + } + + /** + * Creates an error response + * @param requestId The request ID + * @param code The error code + * @param message The error message + * @param details Optional error details + * @returns An MCP response + */ + private createErrorResponse( + requestId: string, + code: string, + message: string, + details?: any + ): MCPResponse { + return { + success: false, + requestId, + version: MCP_VERSION, + error: { + code, + message, + details + } + } + } + + /** + * Creates a new request ID + * @returns A new UUID + */ + generateRequestId(): string { + return uuidv4() + } + + /** + * Handles an MCP request directly (for in-process models) + * @param request The MCP request + * @returns An MCP response + */ + async handleMCPRequest(request: MCPRequest): Promise { + return await this.handleRequest(request) + } +} diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 00000000..a7d89dea --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,19 @@ +/** + * Model Control Protocol (MCP) for Brainy + * + * This module provides a Model Control Protocol (MCP) implementation for Brainy, + * allowing external models to access Brainy data and use the augmentation pipeline as tools. + */ + +// Import and re-export the MCP components +import { BrainyMCPAdapter } from './brainyMCPAdapter.js' +import { MCPAugmentationToolset } from './mcpAugmentationToolset.js' +import { BrainyMCPService } from './brainyMCPService.js' + +// Export the MCP components +export { BrainyMCPAdapter } +export { MCPAugmentationToolset } +export { BrainyMCPService } + +// Export the MCP types +export * from '../types/mcpTypes.js' diff --git a/src/mcp/mcpAugmentationToolset.ts b/src/mcp/mcpAugmentationToolset.ts new file mode 100644 index 00000000..1c118e99 --- /dev/null +++ b/src/mcp/mcpAugmentationToolset.ts @@ -0,0 +1,225 @@ +/** + * MCPAugmentationToolset + * + * This class exposes the Brainy augmentation pipeline as tools through the Model Control Protocol (MCP). + * It provides methods for getting available tools and executing tools. + */ + +import { v4 as uuidv4 } from 'uuid' +import { + MCPResponse, + MCPToolExecutionRequest, + MCPTool, + MCP_VERSION +} from '../types/mcpTypes.js' +import { AugmentationType } from '../types/augmentations.js' + +// Import the augmentation pipeline +import { augmentationPipeline } from '../augmentationPipeline.js' + +export class MCPAugmentationToolset { + /** + * Creates a new MCPAugmentationToolset + */ + constructor() { + // No initialization needed + } + + /** + * Handles an MCP tool execution request + * @param request The MCP request + * @returns An MCP response + */ + async handleRequest(request: MCPToolExecutionRequest): Promise { + try { + const { toolName, parameters } = request + + // Extract the augmentation type and method from the tool name + // Tool names are in the format: brainy_{augmentationType}_{method} + const parts = toolName.split('_') + + if (parts.length < 3 || parts[0] !== 'brainy') { + return this.createErrorResponse( + request.requestId, + 'INVALID_TOOL', + `Invalid tool name: ${toolName}. Tool names should be in the format: brainy_{augmentationType}_{method}` + ) + } + + const augmentationType = parts[1] + const method = parts.slice(2).join('_') + + // Validate the augmentation type + if (!this.isValidAugmentationType(augmentationType)) { + return this.createErrorResponse( + request.requestId, + 'INVALID_AUGMENTATION_TYPE', + `Invalid augmentation type: ${augmentationType}` + ) + } + + // Execute the appropriate pipeline based on the augmentation type + const result = await this.executePipeline(augmentationType, method, parameters) + + return this.createSuccessResponse(request.requestId, result) + } catch (error) { + return this.createErrorResponse( + request.requestId, + 'INTERNAL_ERROR', + error instanceof Error ? error.message : String(error) + ) + } + } + + /** + * Gets all available tools + * @returns An array of MCP tools + */ + async getAvailableTools(): Promise { + const tools: MCPTool[] = [] + + // Get all available augmentation types + const augmentationTypes = augmentationPipeline.getAvailableAugmentationTypes() + + for (const type of augmentationTypes) { + // Get all augmentations of this type + const augmentations = augmentationPipeline.getAugmentationsByType(type) + + for (const augmentation of augmentations) { + // Get all methods of this augmentation (excluding private methods and base methods) + const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(augmentation)) + .filter(method => + !method.startsWith('_') && + method !== 'constructor' && + method !== 'initialize' && + method !== 'shutDown' && + method !== 'getStatus' && + typeof augmentation[method] === 'function' + ) + + // Create a tool for each method + for (const method of methods) { + tools.push(this.createToolDefinition(type, augmentation.name, method)) + } + } + } + + return tools + } + + /** + * Creates a tool definition + * @param type The augmentation type + * @param augmentationName The augmentation name + * @param method The method name + * @returns An MCP tool definition + */ + private createToolDefinition(type: string, augmentationName: string, method: string): MCPTool { + return { + name: `brainy_${type}_${method}`, + description: `Access to Brainy's ${type} augmentation '${augmentationName}' method '${method}'`, + parameters: { + type: 'object', + properties: { + args: { + type: 'array', + description: `Arguments for the ${method} method` + }, + options: { + type: 'object', + description: 'Optional execution options' + } + }, + required: ['args'] + } + } + } + + /** + * Executes the appropriate pipeline based on the augmentation type + * @param type The augmentation type + * @param method The method to execute + * @param parameters The parameters for the method + * @returns The result of the pipeline execution + */ + private async executePipeline(type: string, method: string, parameters: any): Promise { + const { args = [], options = {} } = parameters + + switch (type) { + case AugmentationType.SENSE: + return await augmentationPipeline.executeSensePipeline(method, args, options) + case AugmentationType.CONDUIT: + return await augmentationPipeline.executeConduitPipeline(method, args, options) + case AugmentationType.COGNITION: + return await augmentationPipeline.executeCognitionPipeline(method, args, options) + case AugmentationType.MEMORY: + return await augmentationPipeline.executeMemoryPipeline(method, args, options) + case AugmentationType.PERCEPTION: + return await augmentationPipeline.executePerceptionPipeline(method, args, options) + case AugmentationType.DIALOG: + return await augmentationPipeline.executeDialogPipeline(method, args, options) + case AugmentationType.ACTIVATION: + return await augmentationPipeline.executeActivationPipeline(method, args, options) + default: + throw new Error(`Unsupported augmentation type: ${type}`) + } + } + + /** + * Checks if an augmentation type is valid + * @param type The augmentation type to check + * @returns Whether the augmentation type is valid + */ + private isValidAugmentationType(type: string): boolean { + return Object.values(AugmentationType).includes(type as AugmentationType) + } + + /** + * Creates a success response + * @param requestId The request ID + * @param data The response data + * @returns An MCP response + */ + private createSuccessResponse(requestId: string, data: any): MCPResponse { + return { + success: true, + requestId, + version: MCP_VERSION, + data + } + } + + /** + * Creates an error response + * @param requestId The request ID + * @param code The error code + * @param message The error message + * @param details Optional error details + * @returns An MCP response + */ + private createErrorResponse( + requestId: string, + code: string, + message: string, + details?: any + ): MCPResponse { + return { + success: false, + requestId, + version: MCP_VERSION, + error: { + code, + message, + details + } + } + } + + /** + * Creates a new request ID + * @returns A new UUID + */ + generateRequestId(): string { + return uuidv4() + } +} diff --git a/src/pipeline.ts b/src/pipeline.ts new file mode 100644 index 00000000..36580693 --- /dev/null +++ b/src/pipeline.ts @@ -0,0 +1,919 @@ +/** + * Unified Pipeline + * + * This module combines the functionality of the primary augmentation pipeline and the streamlined pipeline + * into a single, unified pipeline system. It provides both the registry functionality of the primary pipeline + * and the simplified execution API of the streamlined pipeline. + */ + +import { + IAugmentation, + AugmentationType, + AugmentationResponse, + IWebSocketSupport, + BrainyAugmentations +} from './types/augmentations.js' +import { AugmentationRegistry, IPipeline } from './types/pipelineTypes.js' +import { isThreadingAvailable } from './utils/environment.js' +import { executeInThread } from './utils/workerUtils.js' +import { executeAugmentation } from './augmentationFactory.js' +import { setDefaultPipeline } from './augmentationRegistry.js' + +/** + * Execution mode for the pipeline + */ +export enum ExecutionMode { + SEQUENTIAL = 'sequential', + PARALLEL = 'parallel', + FIRST_SUCCESS = 'firstSuccess', + FIRST_RESULT = 'firstResult', + THREADED = 'threaded' // Execute in separate threads when available +} + +/** + * Options for pipeline execution + */ +export interface PipelineOptions { + mode?: ExecutionMode; + timeout?: number; + stopOnError?: boolean; + forceThreading?: boolean; // Force threading even if not in THREADED mode + disableThreading?: boolean; // Disable threading even if in THREADED mode +} + +/** + * Default pipeline options + */ +const DEFAULT_PIPELINE_OPTIONS: PipelineOptions = { + mode: ExecutionMode.SEQUENTIAL, + timeout: 30000, + stopOnError: false, + forceThreading: false, + disableThreading: false +} + +/** + * Result of a pipeline execution + */ +export interface PipelineResult { + results: AugmentationResponse[]; + errors: Error[]; + successful: AugmentationResponse[]; +} + +/** + * Pipeline class + * + * Manages multiple augmentations of each type and provides methods to execute them. + * Implements the IPipeline interface to avoid circular dependencies. + */ +export class Pipeline implements IPipeline { + private registry: AugmentationRegistry = { + sense: [], + conduit: [], + cognition: [], + memory: [], + perception: [], + dialog: [], + activation: [], + webSocket: [] + } + + /** + * Register an augmentation with the pipeline + * + * @param augmentation The augmentation to register + * @returns The pipeline instance for chaining + */ + public register(augmentation: T): Pipeline { + let registered = false + + // Check for specific augmentation types + if (this.isAugmentationType( + augmentation, + 'processRawData', + 'listenToFeed' + )) { + this.registry.sense.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'establishConnection', + 'readData', + 'writeData', + 'monitorStream' + )) { + this.registry.conduit.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'reason', + 'infer', + 'executeLogic' + )) { + this.registry.cognition.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'storeData', + 'retrieveData', + 'updateData', + 'deleteData', + 'listDataKeys' + )) { + this.registry.memory.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'interpret', + 'organize', + 'generateVisualization' + )) { + this.registry.perception.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'processUserInput', + 'generateResponse', + 'manageContext' + )) { + this.registry.dialog.push(augmentation) + registered = true + } else if (this.isAugmentationType( + augmentation, + 'triggerAction', + 'generateOutput', + 'interactExternal' + )) { + this.registry.activation.push(augmentation) + registered = true + } + + // Check if the augmentation supports WebSocket + if (this.isAugmentationType( + augmentation, + 'connectWebSocket', + 'sendWebSocketMessage', + 'onWebSocketMessage', + 'closeWebSocket' + )) { + this.registry.webSocket.push(augmentation as IWebSocketSupport) + registered = true + } + + // If the augmentation wasn't registered as any known type, throw an error + if (!registered) { + throw new Error(`Unknown augmentation type: ${augmentation.name}`) + } + + return this + } + + /** + * Unregister an augmentation from the pipeline + * + * @param augmentationName The name of the augmentation to unregister + * @returns The pipeline instance for chaining + */ + public unregister(augmentationName: string): Pipeline { + let found = false + + // Remove from all registries + for (const type in this.registry) { + const typedRegistry = this.registry[type as keyof AugmentationRegistry] + const index = typedRegistry.findIndex(aug => aug.name === augmentationName) + + if (index !== -1) { + typedRegistry.splice(index, 1) + found = true + } + } + + return this + } + + /** + * Initialize all registered augmentations + * + * @returns A promise that resolves when all augmentations are initialized + */ + public async initialize(): Promise { + const allAugmentations = this.getAllAugmentations() + + await Promise.all( + allAugmentations.map(augmentation => + augmentation.initialize().catch(error => { + console.error(`Failed to initialize augmentation ${augmentation.name}:`, error) + }) + ) + ) + } + + /** + * Shut down all registered augmentations + * + * @returns A promise that resolves when all augmentations are shut down + */ + public async shutDown(): Promise { + const allAugmentations = this.getAllAugmentations() + + await Promise.all( + allAugmentations.map(augmentation => + augmentation.shutDown().catch(error => { + console.error(`Failed to shut down augmentation ${augmentation.name}:`, error) + }) + ) + ) + } + + /** + * Get all registered augmentations + * + * @returns An array of all registered augmentations + */ + public getAllAugmentations(): IAugmentation[] { + // Create a Set to avoid duplicates (an augmentation might be in multiple registries) + const allAugmentations = new Set([ + ...this.registry.sense, + ...this.registry.conduit, + ...this.registry.cognition, + ...this.registry.memory, + ...this.registry.perception, + ...this.registry.dialog, + ...this.registry.activation, + ...this.registry.webSocket + ]) + + // Convert back to array + return Array.from(allAugmentations) + } + + /** + * Get all augmentations of a specific type + * + * @param type The type of augmentation to get + * @returns An array of all augmentations of the specified type + */ + public getAugmentationsByType(type: AugmentationType): IAugmentation[] { + switch (type) { + case AugmentationType.SENSE: + return [...this.registry.sense] + case AugmentationType.CONDUIT: + return [...this.registry.conduit] + case AugmentationType.COGNITION: + return [...this.registry.cognition] + case AugmentationType.MEMORY: + return [...this.registry.memory] + case AugmentationType.PERCEPTION: + return [...this.registry.perception] + case AugmentationType.DIALOG: + return [...this.registry.dialog] + case AugmentationType.ACTIVATION: + return [...this.registry.activation] + case AugmentationType.WEBSOCKET: + return [...this.registry.webSocket] + default: + return [] + } + } + + /** + * Get all available augmentation types + * + * @returns An array of all augmentation types that have at least one registered augmentation + */ + public getAvailableAugmentationTypes(): AugmentationType[] { + const availableTypes: AugmentationType[] = [] + + if (this.registry.sense.length > 0) availableTypes.push(AugmentationType.SENSE) + if (this.registry.conduit.length > 0) availableTypes.push(AugmentationType.CONDUIT) + if (this.registry.cognition.length > 0) availableTypes.push(AugmentationType.COGNITION) + if (this.registry.memory.length > 0) availableTypes.push(AugmentationType.MEMORY) + if (this.registry.perception.length > 0) availableTypes.push(AugmentationType.PERCEPTION) + if (this.registry.dialog.length > 0) availableTypes.push(AugmentationType.DIALOG) + if (this.registry.activation.length > 0) availableTypes.push(AugmentationType.ACTIVATION) + if (this.registry.webSocket.length > 0) availableTypes.push(AugmentationType.WEBSOCKET) + + return availableTypes + } + + /** + * Get all WebSocket-supporting augmentations + * + * @returns An array of all augmentations that support WebSocket connections + */ + public getWebSocketAugmentations(): IWebSocketSupport[] { + return [...this.registry.webSocket] + } + + /** + * Check if an augmentation is of a specific type + * + * @param augmentation The augmentation to check + * @param methods The methods that should be present on the augmentation + * @returns True if the augmentation is of the specified type + */ + private isAugmentationType( + augmentation: IAugmentation, + ...methods: (keyof T)[] + ): augmentation is T { + // First check that the augmentation has all the required base methods + const baseMethodsExist = [ + 'initialize', + 'shutDown', + 'getStatus' + ].every(method => typeof (augmentation as any)[method] === 'function') + + if (!baseMethodsExist) { + return false + } + + // Then check that it has all the specific methods for this type + return methods.every(method => typeof (augmentation as any)[method] === 'function') + } + + /** + * Determines if threading should be used based on options and environment + * + * @param options The pipeline options + * @returns True if threading should be used, false otherwise + */ + private shouldUseThreading(options: PipelineOptions): boolean { + // If threading is explicitly disabled, don't use it + if (options.disableThreading) { + return false + } + + // If threading is explicitly forced, use it if available + if (options.forceThreading) { + return isThreadingAvailable() + } + + // If in THREADED mode, use threading if available + if (options.mode === ExecutionMode.THREADED) { + return isThreadingAvailable() + } + + // Otherwise, don't use threading + return false + } + + /** + * Executes a method on multiple augmentations using the specified execution mode + * + * @param augmentations The augmentations to execute the method on + * @param method The method to execute + * @param args The arguments to pass to the method + * @param options Options for the execution + * @returns A promise that resolves with the results + */ + public async execute( + augmentations: IAugmentation[], + method: string, + args: any[] = [], + options: PipelineOptions = {} + ): Promise> { + const opts = { ...DEFAULT_PIPELINE_OPTIONS, ...options } + const enabledAugmentations = augmentations.filter(aug => aug.enabled !== false) + + if (enabledAugmentations.length === 0) { + return { results: [], errors: [], successful: [] } + } + + const result: PipelineResult = { + results: [], + errors: [], + successful: [] + } + + // Create a function to execute with timeout + const executeWithTimeout = async ( + augmentation: IAugmentation + ): Promise> => { + try { + // Create a timeout promise if a timeout is specified + if (opts.timeout) { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject( + new Error(`Timeout executing ${method} on ${augmentation.name}`) + ) + }, opts.timeout) + }) + + // Check if threading should be used + const useThreading = this.shouldUseThreading(opts) + + // Execute the method on the augmentation, using threading if appropriate + let methodPromise: Promise> + + if (useThreading) { + // Execute in a separate thread + try { + // Create a function that can be serialized and executed in a worker + const workerFn = (...workerArgs: any[]) => { + // This function will be stringified and executed in the worker + // It needs to be self-contained + const augFn = augmentation[method as string] as Function + return augFn.apply(augmentation, workerArgs) + } + + methodPromise = executeInThread>(workerFn.toString(), args) + } catch (threadError) { + console.warn(`Failed to execute in thread, falling back to main thread: ${threadError}`) + // Fall back to executing in the main thread + methodPromise = Promise.resolve((augmentation[method] as Function)(...args) as AugmentationResponse) + } + } else { + // Execute in the main thread + methodPromise = Promise.resolve((augmentation[method] as Function)(...args) as AugmentationResponse) + } + + // Race the method promise against the timeout promise + return await Promise.race([methodPromise, timeoutPromise]) + } else { + // No timeout, just execute the method + return await executeAugmentation(augmentation, method, ...args) + } + } catch (error) { + result.errors.push( + error instanceof Error ? error : new Error(String(error)) + ) + return { + success: false, + data: null as any, + error: error instanceof Error ? error.message : String(error) + } + } + } + + // Execute based on the specified mode + switch (opts.mode) { + case ExecutionMode.PARALLEL: + case ExecutionMode.THREADED: + // Execute all augmentations in parallel + result.results = await Promise.all( + enabledAugmentations.map((aug) => executeWithTimeout(aug)) + ) + break + + case ExecutionMode.FIRST_SUCCESS: + // Execute augmentations sequentially until one succeeds + for (const augmentation of enabledAugmentations) { + const response = await executeWithTimeout(augmentation) + result.results.push(response) + + if (response.success) { + break + } + } + break + + case ExecutionMode.FIRST_RESULT: + // Execute augmentations sequentially until one returns a non-null result + for (const augmentation of enabledAugmentations) { + const response = await executeWithTimeout(augmentation) + result.results.push(response) + + if ( + response.success && + response.data !== null && + response.data !== undefined + ) { + break + } + } + break + + case ExecutionMode.SEQUENTIAL: + default: + // Execute augmentations sequentially + for (const augmentation of enabledAugmentations) { + const response = await executeWithTimeout(augmentation) + result.results.push(response) + + // Check if we need to stop on error + if (opts.stopOnError && !response.success) { + break + } + } + break + } + + // Filter successful results + result.successful = result.results.filter((r) => r.success) + + return result + } + + /** + * Executes a method on augmentations of a specific type + * + * @param type The type of augmentations to execute the method on + * @param method The method to execute + * @param args The arguments to pass to the method + * @param options Options for the execution + * @returns A promise that resolves with the results + */ + public async executeByType( + type: AugmentationType, + method: string, + args: any[] = [], + options: PipelineOptions = {} + ): Promise> { + const augmentations = this.getAugmentationsByType(type) + return this.execute(augmentations, method, args, options) + } + + /** + * Executes a method on a single augmentation with automatic error handling + * + * @param augmentation The augmentation to execute the method on + * @param method The method to execute + * @param args The arguments to pass to the method + * @returns A promise that resolves with the result + */ + public async executeSingle( + augmentation: IAugmentation, + method: string, + ...args: any[] + ): Promise> { + return executeAugmentation(augmentation, method, ...args) + } + + /** + * Process static data through a pipeline of augmentations + * + * @param data The data to process + * @param pipeline An array of processing steps, each with an augmentation, method, and optional args transformer + * @param options Options for the execution + * @returns A promise that resolves with the final result + */ + public async processStaticData( + data: T, + pipeline: Array<{ + augmentation: IAugmentation + method: string + transformArgs?: (data: any, prevResult?: any) => any[] + }>, + options: PipelineOptions = {} + ): Promise> { + let currentData = data + let prevResult: any = undefined + + for (const step of pipeline) { + // Transform args if a transformer is provided, otherwise use the current data as the only arg + const args = step.transformArgs + ? step.transformArgs(currentData, prevResult) + : [currentData] + + // Execute the method + const result = await this.executeSingle( + step.augmentation, + step.method, + ...args + ) + + // If the step failed, return the error + if (!result.success) { + return result as AugmentationResponse + } + + // Update the current data for the next step + currentData = result.data + prevResult = result + } + + // Return the final result + return prevResult as AugmentationResponse + } + + /** + * Process streaming data through a pipeline of augmentations + * + * @param source The source augmentation that provides the data stream + * @param sourceMethod The method on the source augmentation that provides the data stream + * @param sourceArgs The arguments to pass to the source method + * @param pipeline An array of processing steps, each with an augmentation, method, and optional args transformer + * @param callback Function to call with the results of processing each data item + * @param options Options for the execution + * @returns A promise that resolves when the pipeline is set up + */ + public async processStreamingData( + source: IAugmentation, + sourceMethod: string, + sourceArgs: any[], + pipeline: Array<{ + augmentation: IAugmentation + method: string + transformArgs?: (data: any, prevResult?: any) => any[] + }>, + callback: (result: AugmentationResponse) => void, + options: PipelineOptions = {} + ): Promise { + // Create a chain of processors + const processData = async (data: any) => { + let currentData = data + let prevResult: any = undefined + + for (const step of pipeline) { + // Transform args if a transformer is provided, otherwise use the current data as the only arg + const args = step.transformArgs + ? step.transformArgs(currentData, prevResult) + : [currentData] + + // Execute the method + const result = await this.executeSingle( + step.augmentation, + step.method, + ...args + ) + + // If the step failed, return the error + if (!result.success) { + callback(result as AugmentationResponse) + return + } + + // Update the current data for the next step + currentData = result.data + prevResult = result + } + + // Call the callback with the final result + callback(prevResult as AugmentationResponse) + } + + // The last argument to the source method should be a callback that receives the data + const dataCallback = (data: any) => { + processData(data).catch((error) => { + console.error('Error processing streaming data:', error) + callback({ + success: false, + data: null as any, + error: error instanceof Error ? error.message : String(error) + }) + }) + } + + // Execute the source method with the provided args and the data callback + await this.executeSingle(source, sourceMethod, ...sourceArgs, dataCallback) + } + + /** + * Create a reusable pipeline for processing data + * + * @param pipeline An array of processing steps + * @param options Options for the execution + * @returns A function that processes data through the pipeline + */ + public createPipeline( + pipeline: Array<{ + augmentation: IAugmentation + method: string + transformArgs?: (data: any, prevResult?: any) => any[] + }>, + options: PipelineOptions = {} + ): (data: T) => Promise> { + return (data: T) => this.processStaticData(data, pipeline, options) + } + + /** + * Create a reusable streaming pipeline + * + * @param source The source augmentation + * @param sourceMethod The method on the source augmentation + * @param pipeline An array of processing steps + * @param options Options for the execution + * @returns A function that sets up the streaming pipeline + */ + public createStreamingPipeline( + source: IAugmentation, + sourceMethod: string, + pipeline: Array<{ + augmentation: IAugmentation + method: string + transformArgs?: (data: any, prevResult?: any) => any[] + }>, + options: PipelineOptions = {} + ): ( + sourceArgs: any[], + callback: (result: AugmentationResponse) => void + ) => Promise { + return ( + sourceArgs: any[], + callback: (result: AugmentationResponse) => void + ) => + this.processStreamingData( + source, + sourceMethod, + sourceArgs, + pipeline, + callback, + options + ) + } + + // Legacy methods for backward compatibility + + /** + * Execute a sense pipeline (legacy method) + */ + public async executeSensePipeline< + M extends keyof BrainyAugmentations.ISenseAugmentation & string, + R extends BrainyAugmentations.ISenseAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.ISenseAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const result = await this.executeByType(AugmentationType.SENSE, method, args, options) + return result.results.map(r => Promise.resolve(r)) + } + + /** + * Execute a conduit pipeline (legacy method) + */ + public async executeConduitPipeline< + M extends keyof BrainyAugmentations.IConduitAugmentation & string, + R extends BrainyAugmentations.IConduitAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IConduitAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const result = await this.executeByType(AugmentationType.CONDUIT, method, args, options) + return result.results.map(r => Promise.resolve(r)) + } + + /** + * Execute a cognition pipeline (legacy method) + */ + public async executeCognitionPipeline< + M extends keyof BrainyAugmentations.ICognitionAugmentation & string, + R extends BrainyAugmentations.ICognitionAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.ICognitionAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const result = await this.executeByType(AugmentationType.COGNITION, method, args, options) + return result.results.map(r => Promise.resolve(r)) + } + + /** + * Execute a memory pipeline (legacy method) + */ + public async executeMemoryPipeline< + M extends keyof BrainyAugmentations.IMemoryAugmentation & string, + R extends BrainyAugmentations.IMemoryAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IMemoryAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const result = await this.executeByType(AugmentationType.MEMORY, method, args, options) + return result.results.map(r => Promise.resolve(r)) + } + + /** + * Execute a perception pipeline (legacy method) + */ + public async executePerceptionPipeline< + M extends keyof BrainyAugmentations.IPerceptionAugmentation & string, + R extends BrainyAugmentations.IPerceptionAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IPerceptionAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const result = await this.executeByType(AugmentationType.PERCEPTION, method, args, options) + return result.results.map(r => Promise.resolve(r)) + } + + /** + * Execute a dialog pipeline (legacy method) + */ + public async executeDialogPipeline< + M extends keyof BrainyAugmentations.IDialogAugmentation & string, + R extends BrainyAugmentations.IDialogAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IDialogAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const result = await this.executeByType(AugmentationType.DIALOG, method, args, options) + return result.results.map(r => Promise.resolve(r)) + } + + /** + * Execute an activation pipeline (legacy method) + */ + public async executeActivationPipeline< + M extends keyof BrainyAugmentations.IActivationAugmentation & string, + R extends BrainyAugmentations.IActivationAugmentation[M] extends (...args: any[]) => AugmentationResponse ? U : never + >( + method: M & (BrainyAugmentations.IActivationAugmentation[M] extends (...args: any[]) => any ? M : never), + args: Parameters any>>, + options: PipelineOptions = {} + ): Promise[]> { + const result = await this.executeByType(AugmentationType.ACTIVATION, method, args, options) + return result.results.map(r => Promise.resolve(r)) + } +} + +// Create and export a default instance of the pipeline +export const pipeline = new Pipeline() + +// Set the default pipeline instance for the augmentation registry +// This breaks the circular dependency between pipeline.ts and augmentationRegistry.ts +setDefaultPipeline(pipeline) + +// Re-export the legacy pipeline for backward compatibility +export const augmentationPipeline = pipeline + +// Re-export the streamlined execution functions for backward compatibility +export const executeStreamlined = ( + augmentations: IAugmentation[], + method: string, + args: any[] = [], + options: PipelineOptions = {} +): Promise> => { + return pipeline.execute(augmentations, method, args, options) +} + +export const executeByType = ( + type: AugmentationType, + method: string, + args: any[] = [], + options: PipelineOptions = {} +): Promise> => { + return pipeline.executeByType(type, method, args, options) +} + +export const executeSingle = ( + augmentation: IAugmentation, + method: string, + ...args: any[] +): Promise> => { + return pipeline.executeSingle(augmentation, method, ...args) +} + +export const processStaticData = ( + data: T, + pipelineSteps: Array<{ + augmentation: IAugmentation + method: string + transformArgs?: (data: any, prevResult?: any) => any[] + }>, + options: PipelineOptions = {} +): Promise> => { + return pipeline.processStaticData(data, pipelineSteps, options) +} + +export const processStreamingData = ( + source: IAugmentation, + sourceMethod: string, + sourceArgs: any[], + pipelineSteps: Array<{ + augmentation: IAugmentation + method: string + transformArgs?: (data: any, prevResult?: any) => any[] + }>, + callback: (result: AugmentationResponse) => void, + options: PipelineOptions = {} +): Promise => { + return pipeline.processStreamingData(source, sourceMethod, sourceArgs, pipelineSteps, callback, options) +} + +export const createPipeline = ( + pipelineSteps: Array<{ + augmentation: IAugmentation + method: string + transformArgs?: (data: any, prevResult?: any) => any[] + }>, + options: PipelineOptions = {} +): (data: T) => Promise> => { + return pipeline.createPipeline(pipelineSteps, options) +} + +export const createStreamingPipeline = ( + source: IAugmentation, + sourceMethod: string, + pipelineSteps: Array<{ + augmentation: IAugmentation + method: string + transformArgs?: (data: any, prevResult?: any) => any[] + }>, + options: PipelineOptions = {} +): ( + sourceArgs: any[], + callback: (result: AugmentationResponse) => void +) => Promise => { + return pipeline.createStreamingPipeline(source, sourceMethod, pipelineSteps, options) +} + +// For backward compatibility with StreamlinedExecutionMode +export const StreamlinedExecutionMode = ExecutionMode +export type StreamlinedPipelineOptions = PipelineOptions +export type StreamlinedPipelineResult = PipelineResult diff --git a/src/sequentialPipeline.ts b/src/sequentialPipeline.ts new file mode 100644 index 00000000..7d5e727d --- /dev/null +++ b/src/sequentialPipeline.ts @@ -0,0 +1,572 @@ +/** + * Sequential Augmentation Pipeline + * + * This module provides a pipeline for executing augmentations in a specific sequence: + * ISense -> IMemory -> ICognition -> IConduit -> IActivation -> IPerception + * + * It supports high-performance streaming data from WebSockets without blocking. + * Optimized for Node.js 23.11+ using native WebStreams API. + */ + +import { + AugmentationType, + IAugmentation, + IWebSocketSupport, + ISenseAugmentation, + IMemoryAugmentation, + ICognitionAugmentation, + IConduitAugmentation, + IActivationAugmentation, + IPerceptionAugmentation, + AugmentationResponse, + WebSocketConnection +} from './types/augmentations.js' +import { BrainyData } from './brainyData.js' +import { augmentationPipeline } from './augmentationPipeline.js' +// Use the browser's built-in WebStreams API or Node.js native WebStreams API +// This approach ensures compatibility with both environments +let TransformStream: any, ReadableStream: any, WritableStream: any; + +// Function to initialize the stream classes +const initializeStreamClasses = () => { + // Try to use the browser's built-in WebStreams API first + if (typeof globalThis.TransformStream !== 'undefined' && + typeof globalThis.ReadableStream !== 'undefined' && + typeof globalThis.WritableStream !== 'undefined') { + TransformStream = globalThis.TransformStream; + ReadableStream = globalThis.ReadableStream; + WritableStream = globalThis.WritableStream; + return Promise.resolve(); + } else { + // In Node.js environment, try to import from node:stream/web + // This will be executed in Node.js but not in browsers + return import('node:stream/web') + .then(streamWebModule => { + TransformStream = streamWebModule.TransformStream; + ReadableStream = streamWebModule.ReadableStream; + WritableStream = streamWebModule.WritableStream; + }) + .catch(error => { + console.error('Failed to import WebStreams API:', error); + // Provide fallback implementations or throw a more helpful error + throw new Error('WebStreams API is not available in this environment. Please use a modern browser or Node.js 18+.'); + }); + } +}; + +// Initialize immediately but don't block module execution +const streamClassesPromise = initializeStreamClasses(); + +/** + * Options for sequential pipeline execution + */ +export interface SequentialPipelineOptions { + /** + * Timeout for each augmentation execution in milliseconds + */ + timeout?: number; + + /** + * Whether to stop execution if an error occurs + */ + stopOnError?: boolean; + + /** + * BrainyData instance to use for storage + */ + brainyData?: BrainyData; +} + +/** + * Default pipeline options + */ +const DEFAULT_SEQUENTIAL_PIPELINE_OPTIONS: SequentialPipelineOptions = { + timeout: 30000, + stopOnError: false +} + +/** + * Result of a pipeline execution + */ +export interface PipelineResult { + /** + * Whether the pipeline execution was successful + */ + success: boolean; + + /** + * The data returned by the pipeline + */ + data: T; + + /** + * Error message if the pipeline execution failed + */ + error?: string; + + /** + * Results from each stage of the pipeline + */ + stageResults: { + sense?: AugmentationResponse; + memory?: AugmentationResponse; + cognition?: AugmentationResponse; + conduit?: AugmentationResponse; + activation?: AugmentationResponse; + perception?: AugmentationResponse; + } +} + +/** + * SequentialPipeline class + * + * Executes augmentations in a specific sequence: + * ISense -> IMemory -> ICognition -> IConduit -> IActivation -> IPerception + */ +export class SequentialPipeline { + private brainyData: BrainyData; + + /** + * Create a new sequential pipeline + * + * @param options Options for the pipeline + */ + constructor(options: SequentialPipelineOptions = {}) { + this.brainyData = options.brainyData || new BrainyData(); + } + + /** + * Ensure stream classes are initialized + * @private + */ + private async ensureStreamClassesInitialized(): Promise { + await streamClassesPromise; + } + + /** + * Initialize the pipeline + * + * @returns A promise that resolves when initialization is complete + */ + public async initialize(): Promise { + // Initialize stream classes and BrainyData in parallel + await Promise.all([ + this.ensureStreamClassesInitialized(), + this.brainyData.init() + ]); + } + + /** + * Process data through the sequential pipeline + * + * @param rawData The raw data to process + * @param dataType The type of data (e.g., 'text', 'image', 'audio') + * @param options Options for pipeline execution + * @returns A promise that resolves with the pipeline result + */ + public async processData( + rawData: Buffer | string, + dataType: string, + options: SequentialPipelineOptions = {} + ): Promise> { + const opts = { ...DEFAULT_SEQUENTIAL_PIPELINE_OPTIONS, ...options }; + const result: PipelineResult = { + success: true, + data: null, + stageResults: {} + }; + + try { + // Step 1: Process raw data with ISense augmentations + const senseResults = await augmentationPipeline.executeSensePipeline( + 'processRawData', + [rawData, dataType], + { timeout: opts.timeout, stopOnError: opts.stopOnError } + ); + + // Get the first successful result + let senseResult: AugmentationResponse<{ nouns: string[], verbs: string[] }> | null = null; + for (const resultPromise of senseResults) { + const res = await resultPromise; + if (res.success) { + senseResult = res; + break; + } + } + + if (!senseResult || !senseResult.success) { + return { + success: false, + data: null, + error: 'Failed to process raw data with ISense augmentations', + stageResults: { sense: senseResult || { success: false, data: null, error: 'No sense augmentations available' } } + }; + } + + result.stageResults.sense = senseResult; + + // Step 2: Store data in BrainyData using IMemory augmentations + const memoryAugmentations = augmentationPipeline.getAugmentationsByType(AugmentationType.MEMORY) as IMemoryAugmentation[]; + + if (memoryAugmentations.length === 0) { + return { + success: false, + data: null, + error: 'No memory augmentations available', + stageResults: result.stageResults + }; + } + + // Use the first available memory augmentation + const memoryAugmentation = memoryAugmentations[0]; + + // Generate a key for the data + const dataKey = `data_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; + + // Store the data + const memoryResult = await memoryAugmentation.storeData( + dataKey, + { + rawData, + dataType, + nouns: senseResult.data.nouns, + verbs: senseResult.data.verbs, + timestamp: Date.now() + } + ); + + if (!memoryResult.success) { + return { + success: false, + data: null, + error: `Failed to store data: ${memoryResult.error}`, + stageResults: { ...result.stageResults, memory: memoryResult } + }; + } + + result.stageResults.memory = memoryResult; + + // Step 3: Trigger ICognition augmentations to analyze the data + const cognitionResults = await augmentationPipeline.executeCognitionPipeline( + 'reason', + [`Analyze data with key ${dataKey}`, { dataKey }], + { timeout: opts.timeout, stopOnError: opts.stopOnError } + ); + + // Get the first successful result + let cognitionResult: AugmentationResponse<{ inference: string, confidence: number }> | null = null; + for (const resultPromise of cognitionResults) { + const res = await resultPromise; + if (res.success) { + cognitionResult = res; + break; + } + } + + if (cognitionResult) { + result.stageResults.cognition = cognitionResult; + } + + // Step 4: Send notifications to IConduit augmentations + const conduitResults = await augmentationPipeline.executeConduitPipeline( + 'writeData', + [{ dataKey, nouns: senseResult.data.nouns, verbs: senseResult.data.verbs }], + { timeout: opts.timeout, stopOnError: opts.stopOnError } + ); + + // Get the first successful result + let conduitResult: AugmentationResponse | null = null; + for (const resultPromise of conduitResults) { + const res = await resultPromise; + if (res.success) { + conduitResult = res; + break; + } + } + + if (conduitResult) { + result.stageResults.conduit = conduitResult; + } + + // Step 5: Send notifications to IActivation augmentations + const activationResults = await augmentationPipeline.executeActivationPipeline( + 'triggerAction', + ['dataProcessed', { dataKey }], + { timeout: opts.timeout, stopOnError: opts.stopOnError } + ); + + // Get the first successful result + let activationResult: AugmentationResponse | null = null; + for (const resultPromise of activationResults) { + const res = await resultPromise; + if (res.success) { + activationResult = res; + break; + } + } + + if (activationResult) { + result.stageResults.activation = activationResult; + } + + // Step 6: Send notifications to IPerception augmentations + const perceptionResults = await augmentationPipeline.executePerceptionPipeline( + 'interpret', + [senseResult.data.nouns, senseResult.data.verbs, { dataKey }], + { timeout: opts.timeout, stopOnError: opts.stopOnError } + ); + + // Get the first successful result + let perceptionResult: AugmentationResponse> | null = null; + for (const resultPromise of perceptionResults) { + const res = await resultPromise; + if (res.success) { + perceptionResult = res; + break; + } + } + + if (perceptionResult) { + result.stageResults.perception = perceptionResult; + result.data = perceptionResult.data; + } else { + // If no perception result, use the cognition result as the final data + result.data = cognitionResult ? cognitionResult.data : { dataKey }; + } + + return result; + } catch (error: unknown) { + return { + success: false, + data: null, + error: `Pipeline execution failed: ${error}`, + stageResults: result.stageResults + }; + } + } + + /** + * Process WebSocket data through the sequential pipeline + * + * @param connection The WebSocket connection + * @param dataType The type of data (e.g., 'text', 'image', 'audio') + * @param options Options for pipeline execution + * @returns A function to handle incoming WebSocket messages + */ + public async createWebSocketHandler( + connection: WebSocketConnection, + dataType: string, + options: SequentialPipelineOptions = {} + ): Promise<(data: unknown) => void> { + // Ensure stream classes are initialized + await this.ensureStreamClassesInitialized(); + + // Create a transform stream for processing data + const transformStream = new TransformStream({ + transform: async (chunk: unknown, controller: TransformStreamDefaultController) => { + try { + const data = typeof chunk === 'string' ? chunk : JSON.stringify(chunk); + const result = await this.processData(data, dataType, options); + if (result.success) { + controller.enqueue(result); + } else { + console.warn('Pipeline processing failed:', result.error); + } + } catch (error: unknown) { + console.error('Error in transform stream:', error); + } + } + }); + + // Create a writable stream that will be the sink for our data + const writableStream = new WritableStream({ + write: async (result: PipelineResult) => { + // Handle the processed result if needed + if (connection.send && typeof connection.send === 'function') { + try { + // Only send back results if the connection supports it + await connection.send(JSON.stringify(result)); + } catch (error: unknown) { + console.error('Error sending result back to WebSocket:', error); + } + } + } + }); + + // Connect the transform stream to the writable stream + transformStream.readable.pipeTo(writableStream).catch((error: Error) => { + console.error('Error in pipeline stream:', error); + }); + + // Return a function that writes to the transform stream + return (data: unknown) => { + try { + // Write to the transform stream's writable side + const writer = transformStream.writable.getWriter(); + writer.write(data).catch((error: Error) => { + console.error('Error writing to stream:', error); + }).finally(() => { + writer.releaseLock(); + }); + } catch (error: unknown) { + console.error('Error getting writer for transform stream:', error); + } + }; + } + + /** + * Set up a WebSocket connection to process data through the pipeline + * + * @param url The WebSocket URL to connect to + * @param dataType The type of data (e.g., 'text', 'image', 'audio') + * @param options Options for pipeline execution + * @returns A promise that resolves with the WebSocket connection and associated streams + */ + public async setupWebSocketPipeline( + url: string, + dataType: string, + options: SequentialPipelineOptions = {} + ): Promise, + writableStream?: WritableStream + }> { + // Ensure stream classes are initialized + await this.ensureStreamClassesInitialized(); + + // Get WebSocket-supporting augmentations + const webSocketAugmentations = augmentationPipeline.getWebSocketAugmentations(); + + if (webSocketAugmentations.length === 0) { + throw new Error('No WebSocket-supporting augmentations available'); + } + + // Use the first available WebSocket augmentation + const webSocketAugmentation = webSocketAugmentations[0]; + + // Connect to the WebSocket + const connection = await webSocketAugmentation.connectWebSocket(url); + + // Create a readable stream from the WebSocket messages + const readableStream = new ReadableStream({ + start: (controller: ReadableStreamDefaultController) => { + // Define a message handler that writes to the stream + const messageHandler = (event: { data: unknown }) => { + try { + const data = typeof event.data === 'string' + ? event.data + : event.data instanceof Blob + ? new Promise(resolve => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsText(event.data as Blob); + }) + : JSON.stringify(event.data); + + // Handle both string data and promises + if (data instanceof Promise) { + data.then(resolvedData => { + controller.enqueue(resolvedData); + }).catch((error: Error) => { + console.error('Error processing blob data:', error); + }); + } else { + controller.enqueue(data); + } + } catch (error: unknown) { + console.error('Error processing WebSocket message:', error); + } + }; + + // Create a wrapper function that adapts the event-based handler to the data-based callback + const messageHandlerWrapper = (data: unknown) => { + messageHandler({ data }); + }; + + // Store both handlers for later cleanup + connection._streamMessageHandler = messageHandler; + connection._messageHandlerWrapper = messageHandlerWrapper; + + webSocketAugmentation.onWebSocketMessage( + connection.connectionId, + messageHandlerWrapper + ).catch((error: Error) => { + console.error('Error registering WebSocket message handler:', error); + controller.error(error); + }); + }, + cancel: () => { + // Clean up the message handler when the stream is cancelled + if (connection._messageHandlerWrapper) { + webSocketAugmentation.offWebSocketMessage( + connection.connectionId, + connection._messageHandlerWrapper + ).catch((error: Error) => { + console.error('Error removing WebSocket message handler:', error); + }); + delete connection._streamMessageHandler; + delete connection._messageHandlerWrapper; + } + } + }); + + // Create a handler for processing the data + const handlerPromise = this.createWebSocketHandler(connection, dataType, options); + + // Create a writable stream that sends data to the WebSocket + const writableStream = new WritableStream({ + write: async (chunk: unknown) => { + if (connection.send && typeof connection.send === 'function') { + try { + const data = typeof chunk === 'string' ? chunk : JSON.stringify(chunk); + await connection.send(data); + } catch (error: unknown) { + console.error('Error sending data to WebSocket:', error); + throw error; + } + } else { + throw new Error('WebSocket connection does not support sending data'); + } + }, + close: () => { + // Close the WebSocket connection when the stream is closed + if (connection.close && typeof connection.close === 'function') { + connection.close().catch((error: Error) => { + console.error('Error closing WebSocket connection:', error); + }); + } + } + }); + + // Pipe the readable stream through our processing pipeline + readableStream + .pipeThrough(new TransformStream({ + transform: async (chunk: unknown, controller: TransformStreamDefaultController) => { + // Process each chunk through our handler + const handler = await handlerPromise; + handler(chunk); + // Pass through the original data + controller.enqueue(chunk); + } + })) + .pipeTo(new WritableStream({ + write: () => {}, + abort: (error: Error) => { + console.error('Error in WebSocket pipeline:', error); + } + })); + + // Attach the streams to the connection object for convenience + const enhancedConnection = connection as WebSocketConnection & { + readableStream: ReadableStream, + writableStream: WritableStream + }; + + enhancedConnection.readableStream = readableStream; + enhancedConnection.writableStream = writableStream; + + return enhancedConnection; + } +} + +// Create and export a default instance of the sequential pipeline +export const sequentialPipeline = new SequentialPipeline(); diff --git a/src/storage/fileSystemStorage.ts b/src/storage/fileSystemStorage.ts new file mode 100644 index 00000000..ecd9f35e --- /dev/null +++ b/src/storage/fileSystemStorage.ts @@ -0,0 +1,931 @@ +import { GraphVerb, HNSWNoun, StorageAdapter } from '../coreTypes.js' + +// We'll dynamically import Node.js built-in modules +let fs: any +let path: any + +// Constants for directory and file names +const ROOT_DIR = 'brainy-data' +const NOUNS_DIR = 'nouns' +const VERBS_DIR = 'verbs' +const METADATA_DIR = 'metadata' + +// Constants for noun type directories +const PERSON_DIR = 'person' +const PLACE_DIR = 'place' +const THING_DIR = 'thing' +const EVENT_DIR = 'event' +const CONCEPT_DIR = 'concept' +const CONTENT_DIR = 'content' +const DEFAULT_DIR = 'default' // For nodes without a noun type + +/** + * File system storage adapter for Node.js environments + */ +export class FileSystemStorage implements StorageAdapter { + private rootDir: string + private nounsDir: string + private verbsDir: string + private metadataDir: string + private personDir: string + private placeDir: string + private thingDir: string + private eventDir: string + private conceptDir: string + private contentDir: string + private defaultDir: string + private isInitialized = false + + constructor(rootDirectory?: string) { + // We'll set the paths in the init method after dynamically importing the modules + this.rootDir = rootDirectory || '' + this.nounsDir = '' + this.verbsDir = '' + this.metadataDir = '' + this.personDir = '' + this.placeDir = '' + this.thingDir = '' + this.eventDir = '' + this.conceptDir = '' + this.contentDir = '' + this.defaultDir = '' + } + + /** + * Initialize the storage adapter + */ + public async init(): Promise { + if (this.isInitialized) { + return + } + + try { + // Dynamically import Node.js built-in modules + try { + // Import the modules + const fsModule = await import('fs') + const pathModule = await import('path') + + // Assign to our module-level variables + fs = fsModule.default || fsModule + path = pathModule.default || pathModule + + // Now set up the directory paths + const rootDir = this.rootDir || process.cwd() + this.rootDir = path.resolve(rootDir, ROOT_DIR) + this.nounsDir = path.join(this.rootDir, NOUNS_DIR) + this.verbsDir = path.join(this.rootDir, VERBS_DIR) + this.metadataDir = path.join(this.rootDir, METADATA_DIR) + + // Set up noun type directory paths + this.personDir = path.join(this.nounsDir, PERSON_DIR) + this.placeDir = path.join(this.nounsDir, PLACE_DIR) + this.thingDir = path.join(this.nounsDir, THING_DIR) + this.eventDir = path.join(this.nounsDir, EVENT_DIR) + this.conceptDir = path.join(this.nounsDir, CONCEPT_DIR) + this.contentDir = path.join(this.nounsDir, CONTENT_DIR) + this.defaultDir = path.join(this.nounsDir, DEFAULT_DIR) + } catch (importError) { + throw new Error(`Failed to import Node.js modules: ${importError}. This adapter requires a Node.js environment.`) + } + + // Create directories if they don't exist + await this.ensureDirectoryExists(this.rootDir) + await this.ensureDirectoryExists(this.nounsDir) + await this.ensureDirectoryExists(this.verbsDir) + await this.ensureDirectoryExists(this.metadataDir) + + // Create noun type directories + await this.ensureDirectoryExists(this.personDir) + await this.ensureDirectoryExists(this.placeDir) + await this.ensureDirectoryExists(this.thingDir) + await this.ensureDirectoryExists(this.eventDir) + await this.ensureDirectoryExists(this.conceptDir) + await this.ensureDirectoryExists(this.contentDir) + await this.ensureDirectoryExists(this.defaultDir) + + this.isInitialized = true + } catch (error) { + console.error('Failed to initialize file system storage:', error) + throw new Error(`Failed to initialize file system storage: ${error}`) + } + } + + /** + * Save a node to storage + */ + public async saveNoun(noun: HNSWNoun): Promise { + await this.ensureInitialized() + + try { + // Convert connections Map to a serializable format + const serializableNode = { + ...noun, + connections: this.mapToObject(noun.connections, (set) => Array.from(set as Set)) + } + + // Get the appropriate directory based on the node's metadata + const nodeDir = await this.getNodeDirectory(noun.id) + + const filePath = path.join(nodeDir, `${noun.id}.json`) + await fs.promises.writeFile( + filePath, + JSON.stringify(serializableNode, null, 2), + 'utf8' + ) + } catch (error) { + console.error(`Failed to save node ${noun.id}:`, error) + throw new Error(`Failed to save node ${noun.id}: ${error}`) + } + } + + /** + * Get a node from storage + */ + public async getNoun(id: string): Promise { + await this.ensureInitialized() + + try { + // Get the appropriate directory based on the node's metadata + const nodeDir = await this.getNodeDirectory(id) + + const filePath = path.join(nodeDir, `${id}.json`) + + // Check if a file exists + try { + await fs.promises.access(filePath) + } catch { + // If the file doesn't exist in the expected directory, try the default directory + if (nodeDir !== this.defaultDir) { + const defaultFilePath = path.join(this.defaultDir, `${id}.json`) + try { + await fs.promises.access(defaultFilePath) + // If found in default directory, use that path + const data = await fs.promises.readFile(defaultFilePath, 'utf8') + const parsedNode = JSON.parse(data) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(parsedNode.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedNode.id, + vector: parsedNode.vector, + connections + } + } catch { + // If not found in default directory either, try all noun type directories + const directories = [ + this.personDir, + this.placeDir, + this.thingDir, + this.eventDir, + this.conceptDir, + this.contentDir + ] + + for (const dir of directories) { + if (dir === nodeDir) continue // Skip the already checked directory + + const dirFilePath = path.join(dir, `${id}.json`) + try { + await fs.promises.access(dirFilePath) + // If found in this directory, use that path + const data = await fs.promises.readFile(dirFilePath, 'utf8') + const parsedNode = JSON.parse(data) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(parsedNode.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedNode.id, + vector: parsedNode.vector, + connections + } + } catch { + // Continue to the next directory + } + } + + return null // File doesn't exist in any directory + } + } + return null // File doesn't exist + } + + const data = await fs.promises.readFile(filePath, 'utf8') + const parsedNode = JSON.parse(data) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(parsedNode.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedNode.id, + vector: parsedNode.vector, + connections + } + } catch (error) { + console.error(`Failed to get node ${id}:`, error) + return null + } + } + + /** + * Get nodes by noun type + * @param nounType The noun type to filter by + * @returns Promise that resolves to an array of nodes of the specified noun type + */ + public async getNounsByNounType(nounType: string): Promise { + await this.ensureInitialized() + + try { + // Determine the directory based on the noun type + let dir: string + switch (nounType) { + case 'person': + dir = this.personDir + break + case 'place': + dir = this.placeDir + break + case 'thing': + dir = this.thingDir + break + case 'event': + dir = this.eventDir + break + case 'concept': + dir = this.conceptDir + break + case 'content': + dir = this.contentDir + break + default: + dir = this.defaultDir + } + + const nodes: HNSWNoun[] = [] + + try { + const files = await fs.promises.readdir(dir) + const nodePromises = files + .filter((file: string) => file.endsWith('.json')) + .map((file: string) => { + // Use the file path directly instead of getNode to avoid redundant searches + return this.readNodeFromFile(path.join(dir, file)) + }) + + const dirNodes = await Promise.all(nodePromises) + nodes.push(...dirNodes.filter((node): node is HNSWNoun => node !== null)) + } catch (dirError) { + // If directory doesn't exist or can't be read, log a warning + console.warn(`Could not read directory for noun type ${nounType}:`, dirError) + } + + return nodes + } catch (error) { + console.error(`Failed to get nodes for noun type ${nounType}:`, error) + throw new Error(`Failed to get nodes for noun type ${nounType}: ${error}`) + } + } + + /** + * Get all nodes from storage + */ + public async getAllNouns(): Promise { + await this.ensureInitialized() + + try { + // Get all noun types + const nounTypes = [ + 'person', + 'place', + 'thing', + 'event', + 'concept', + 'content', + 'default' + ] + + // Run searches in parallel for all noun types + const nodePromises = nounTypes.map(nounType => this.getNounsByNounType(nounType)) + const nodeArrays = await Promise.all(nodePromises) + + // Combine all results + const allNodes: HNSWNoun[] = [] + for (const nodes of nodeArrays) { + allNodes.push(...nodes) + } + + return allNodes + } catch (error) { + console.error('Failed to get all nodes:', error) + throw new Error(`Failed to get all nodes: ${error}`) + } + } + + /** + * Read a node from a file + */ + private async readNodeFromFile(filePath: string): Promise { + try { + const data = await fs.promises.readFile(filePath, 'utf8') + const parsedNode = JSON.parse(data) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(parsedNode.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedNode.id, + vector: parsedNode.vector, + connections + } + } catch (error) { + console.error(`Failed to read node from file ${filePath}:`, error) + return null + } + } + + /** + * Delete a node from storage + */ + public async deleteNoun(id: string): Promise { + await this.ensureInitialized() + + try { + // Get the appropriate directory based on the node's metadata + const nodeDir = await this.getNodeDirectory(id) + + const filePath = path.join(nodeDir, `${id}.json`) + + // Check if a file exists before attempting to delete + try { + await fs.promises.access(filePath) + await fs.promises.unlink(filePath) + return // File found and deleted + } catch { + // If the file doesn't exist in the expected directory, try the default directory + if (nodeDir !== this.defaultDir) { + const defaultFilePath = path.join(this.defaultDir, `${id}.json`) + try { + await fs.promises.access(defaultFilePath) + await fs.promises.unlink(defaultFilePath) + return // File found and deleted + } catch { + // If not found in default directory either, try all noun type directories + const directories = [ + this.personDir, + this.placeDir, + this.thingDir, + this.eventDir, + this.conceptDir, + this.contentDir + ] + + for (const dir of directories) { + if (dir === nodeDir) continue // Skip the already checked directory + + const dirFilePath = path.join(dir, `${id}.json`) + try { + await fs.promises.access(dirFilePath) + await fs.promises.unlink(dirFilePath) + return // File found and deleted + } catch { + // Continue to the next directory + } + } + + return // File doesn't exist in any directory, nothing to delete + } + } + return // File doesn't exist, nothing to delete + } + } catch (error) { + console.error(`Failed to delete node ${id}:`, error) + throw new Error(`Failed to delete node ${id}: ${error}`) + } + } + + /** + * Save an edge to storage + */ + public async saveVerb(verb: GraphVerb): Promise { + await this.ensureInitialized() + + try { + // Convert connections Map to a serializable format + const serializableEdge = { + ...verb, + connections: this.mapToObject(verb.connections, (set) => Array.from(set as Set)) + } + + const filePath = path.join(this.verbsDir, `${verb.id}.json`) + await fs.promises.writeFile( + filePath, + JSON.stringify(serializableEdge, null, 2), + 'utf8' + ) + } catch (error) { + console.error(`Failed to save edge ${verb.id}:`, error) + throw new Error(`Failed to save edge ${verb.id}: ${error}`) + } + } + + /** + * Get an edge from storage + */ + public async getVerb(id: string): Promise { + await this.ensureInitialized() + + try { + const filePath = path.join(this.verbsDir, `${id}.json`) + + // Check if a file exists + try { + await fs.promises.access(filePath) + } catch { + return null // File doesn't exist + } + + const data = await fs.promises.readFile(filePath, 'utf8') + const parsedEdge = JSON.parse(data) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(parsedEdge.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedEdge.id, + vector: parsedEdge.vector, + connections, + sourceId: parsedEdge.sourceId, + targetId: parsedEdge.targetId, + type: parsedEdge.type, + weight: parsedEdge.weight, + metadata: parsedEdge.metadata + } + } catch (error) { + console.error(`Failed to get edge ${id}:`, error) + return null + } + } + + /** + * Get all edges from storage + */ + public async getAllVerbs(): Promise { + await this.ensureInitialized() + + try { + const files = await fs.promises.readdir(this.verbsDir) + const edgePromises = files + .filter((file: string) => file.endsWith('.json')) + .map((file: string) => { + const id = path.basename(file, '.json') + return this.getVerb(id) + }) + + const edges = await Promise.all(edgePromises) + return edges.filter((edge): edge is GraphVerb => edge !== null) + } catch (error) { + console.error('Failed to get all edges:', error) + throw new Error(`Failed to get all edges: ${error}`) + } + } + + /** + * Get edges by source node ID + */ + public async getVerbsBySource(sourceId: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllVerbs() + return allEdges.filter(edge => edge.sourceId === sourceId) + } catch (error) { + console.error(`Failed to get edges by source ${sourceId}:`, error) + throw new Error(`Failed to get edges by source ${sourceId}: ${error}`) + } + } + + /** + * Get edges by target node ID + */ + public async getVerbsByTarget(targetId: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllVerbs() + return allEdges.filter(edge => edge.targetId === targetId) + } catch (error) { + console.error(`Failed to get edges by target ${targetId}:`, error) + throw new Error(`Failed to get edges by target ${targetId}: ${error}`) + } + } + + /** + * Get edges by type + */ + public async getVerbsByType(type: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllVerbs() + return allEdges.filter(edge => edge.type === type) + } catch (error) { + console.error(`Failed to get edges by type ${type}:`, error) + throw new Error(`Failed to get edges by type ${type}: ${error}`) + } + } + + /** + * Delete an edge from storage + */ + public async deleteVerb(id: string): Promise { + await this.ensureInitialized() + + try { + const filePath = path.join(this.verbsDir, `${id}.json`) + + // Check if a file exists before attempting to delete + try { + await fs.promises.access(filePath) + } catch { + return // File doesn't exist, nothing to delete + } + + await fs.promises.unlink(filePath) + } catch (error) { + console.error(`Failed to delete edge ${id}:`, error) + throw new Error(`Failed to delete edge ${id}: ${error}`) + } + } + + /** + * Save metadata to storage + */ + public async saveMetadata(id: string, metadata: any): Promise { + await this.ensureInitialized() + + try { + const filePath = path.join(this.metadataDir, `${id}.json`) + await fs.promises.writeFile( + filePath, + JSON.stringify(metadata, null, 2), + 'utf8' + ) + } catch (error) { + console.error(`Failed to save metadata for ${id}:`, error) + throw new Error(`Failed to save metadata for ${id}: ${error}`) + } + } + + /** + * Get metadata from storage + */ + public async getMetadata(id: string): Promise { + await this.ensureInitialized() + + try { + const filePath = path.join(this.metadataDir, `${id}.json`) + + // Check if a file exists + try { + await fs.promises.access(filePath) + } catch { + return null // File doesn't exist + } + + const data = await fs.promises.readFile(filePath, 'utf8') + return JSON.parse(data) + } catch (error) { + console.error(`Failed to get metadata for ${id}:`, error) + return null + } + } + + /** + * Clear all data from storage + */ + public async clear(): Promise { + await this.ensureInitialized() + + try { + // Delete and recreate the nodes, edges, and metadata directories + await this.deleteDirectory(this.nounsDir) + await this.deleteDirectory(this.verbsDir) + await this.deleteDirectory(this.metadataDir) + + await this.ensureDirectoryExists(this.nounsDir) + await this.ensureDirectoryExists(this.verbsDir) + await this.ensureDirectoryExists(this.metadataDir) + + // Create noun type directories + await this.ensureDirectoryExists(this.personDir) + await this.ensureDirectoryExists(this.placeDir) + await this.ensureDirectoryExists(this.thingDir) + await this.ensureDirectoryExists(this.eventDir) + await this.ensureDirectoryExists(this.conceptDir) + await this.ensureDirectoryExists(this.contentDir) + await this.ensureDirectoryExists(this.defaultDir) + } catch (error) { + console.error('Failed to clear storage:', error) + throw new Error(`Failed to clear storage: ${error}`) + } + } + + /** + * Ensure the storage adapter is initialized + */ + private async ensureInitialized(): Promise { + if (!this.isInitialized) { + await this.init() + } + } + + /** + * Ensure a directory exists, creating it if necessary + */ + private async ensureDirectoryExists(dirPath: string): Promise { + try { + await fs.promises.access(dirPath) + } catch { + // Directory doesn't exist, create it + await fs.promises.mkdir(dirPath, { recursive: true }) + } + } + + /** + * Delete a directory and all its contents recursively + */ + private async deleteDirectory(dirPath: string): Promise { + try { + const files = await fs.promises.readdir(dirPath) + + for (const file of files) { + const filePath = path.join(dirPath, file) + const stats = await fs.promises.stat(filePath) + + if (stats.isDirectory()) { + // Recursively delete subdirectories + await this.deleteDirectory(filePath) + } else { + // Delete files + await fs.promises.unlink(filePath) + } + } + + // After all contents are deleted, remove the directory itself + await fs.promises.rmdir(dirPath) + } catch (error) { + // If the directory doesn't exist, that's fine + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error + } + } + } + + /** + * Count the number of JSON files in a directory + */ + private async countFilesInDirectory(dirPath: string): Promise { + try { + const files = await fs.promises.readdir(dirPath) + return files.filter((file: string) => file.endsWith('.json')).length + } catch (error) { + // If the directory doesn't exist, return 0 + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return 0 + } + throw error + } + } + + /** + * Convert a Map to a plain object for serialization + */ + private mapToObject( + map: Map, + valueTransformer: (value: V) => any = (v) => v + ): Record { + const obj: Record = {} + for (const [key, value] of map.entries()) { + obj[key.toString()] = valueTransformer(value) + } + return obj + } + + /** + * Get the appropriate directory for a node based on its metadata + */ + private async getNodeDirectory(id: string): Promise { + try { + // Try to get the metadata for the node + const metadata = await this.getMetadata(id) + + // If metadata exists and has a noun field, use the corresponding directory + if (metadata && metadata.noun) { + switch (metadata.noun) { + case 'person': + return this.personDir + case 'place': + return this.placeDir + case 'thing': + return this.thingDir + case 'event': + return this.eventDir + case 'concept': + return this.conceptDir + case 'content': + return this.contentDir + default: + return this.defaultDir + } + } + + // If no metadata or no noun field, use the default directory + return this.defaultDir + } catch (error) { + // If there's an error getting the metadata, use the default directory + return this.defaultDir + } + } + + /** + * Get information about storage usage and capacity + */ + public async getStorageStatus(): Promise<{ + type: string; + used: number; + quota: number | null; + details?: Record; + }> { + await this.ensureInitialized() + + try { + // Calculate the total size of all files in the storage directories + let totalSize = 0 + + // Helper function to calculate directory size + const calculateDirSize = async (dirPath: string): Promise => { + let size = 0 + try { + const files = await fs.promises.readdir(dirPath) + + for (const file of files) { + const filePath = path.join(dirPath, file) + const stats = await fs.promises.stat(filePath) + + if (stats.isDirectory()) { + size += await calculateDirSize(filePath) + } else { + size += stats.size + } + } + } catch (error) { + console.warn(`Error calculating size for ${dirPath}:`, error) + } + return size + } + + // Calculate size for each directory + const nodesDirSize = await calculateDirSize(this.nounsDir) + const edgesDirSize = await calculateDirSize(this.verbsDir) + const metadataDirSize = await calculateDirSize(this.metadataDir) + + // Calculate sizes of noun type directories + const personDirSize = await calculateDirSize(this.personDir) + const placeDirSize = await calculateDirSize(this.placeDir) + const thingDirSize = await calculateDirSize(this.thingDir) + const eventDirSize = await calculateDirSize(this.eventDir) + const conceptDirSize = await calculateDirSize(this.conceptDir) + const contentDirSize = await calculateDirSize(this.contentDir) + const defaultDirSize = await calculateDirSize(this.defaultDir) + + // Note: The noun type directories are subdirectories of the nodes directory, + // so their sizes are already included in nodesDirSize. + // We don't need to add them again to avoid double counting. + totalSize = nodesDirSize + edgesDirSize + metadataDirSize + + // Get filesystem information + let quota = null + let details: { + nounTypes?: { + person: { size: number; count: number }; + place: { size: number; count: number }; + thing: { size: number; count: number }; + event: { size: number; count: number }; + concept: { size: number; count: number }; + content: { size: number; count: number }; + default: { size: number; count: number }; + }; + availableSpace?: number; + totalSpace?: number; + freePercentage?: number; + } = { + nounTypes: { + person: { + size: personDirSize, + count: await this.countFilesInDirectory(this.personDir) + }, + place: { + size: placeDirSize, + count: await this.countFilesInDirectory(this.placeDir) + }, + thing: { + size: thingDirSize, + count: await this.countFilesInDirectory(this.thingDir) + }, + event: { + size: eventDirSize, + count: await this.countFilesInDirectory(this.eventDir) + }, + concept: { + size: conceptDirSize, + count: await this.countFilesInDirectory(this.conceptDir) + }, + content: { + size: contentDirSize, + count: await this.countFilesInDirectory(this.contentDir) + }, + default: { + size: defaultDirSize, + count: await this.countFilesInDirectory(this.defaultDir) + } + } + } + + try { + // Try to get disk space information + const stats = await fs.promises.statfs(this.rootDir) + if (stats) { + const availableSpace = stats.bavail * stats.bsize + const totalSpace = stats.blocks * stats.bsize + + quota = totalSpace + details = { + availableSpace, + totalSpace, + freePercentage: (availableSpace / totalSpace) * 100 + } + } + } catch (error) { + console.warn('Unable to get filesystem stats:', error) + // If statfs is not available, try to use df command on Unix-like systems + try { + const { exec } = await import('child_process') + const util = await import('util') + const execPromise = util.promisify(exec) + + const { stdout } = await execPromise(`df -k "${this.rootDir}"`) + const lines = stdout.trim().split('\n') + if (lines.length > 1) { + const parts = lines[1].split(/\s+/) + if (parts.length >= 4) { + const totalKB = parseInt(parts[1], 10) + const usedKB = parseInt(parts[2], 10) + const availableKB = parseInt(parts[3], 10) + + quota = totalKB * 1024 + details = { + availableSpace: availableKB * 1024, + totalSpace: totalKB * 1024, + freePercentage: (availableKB / totalKB) * 100 + } + } + } + } catch (dfError) { + console.warn('Unable to get disk space using df command:', dfError) + } + } + + return { + type: 'filesystem', + used: totalSize, + quota, + details + } + } catch (error) { + console.error('Failed to get storage status:', error) + return { + type: 'filesystem', + used: 0, + quota: null, + details: { error: String(error) } + } + } + } +} diff --git a/src/storage/opfsStorage.ts b/src/storage/opfsStorage.ts new file mode 100644 index 00000000..11914918 --- /dev/null +++ b/src/storage/opfsStorage.ts @@ -0,0 +1,1890 @@ +/** + * OPFS (Origin Private File System) Storage Adapter + * Provides persistent storage for the vector database using the Origin Private File System API + */ + +import { GraphVerb, HNSWNoun, StorageAdapter } from '../coreTypes.js' +import '../types/fileSystemTypes.js' +// Make sure TypeScript recognizes the FileSystemDirectoryHandle interface extension +declare global { + interface FileSystemDirectoryHandle { + entries(): AsyncIterableIterator<[string, FileSystemHandle]> + } +} + +// Type aliases for compatibility +type HNSWNode = HNSWNoun +type Edge = GraphVerb + +// Directory and file names +const ROOT_DIR = 'opfs-vector-db' +const NOUNS_DIR = 'nouns' +const VERBS_DIR = 'verbs' +const METADATA_DIR = 'metadata' +const DB_INFO_FILE = 'db-info.json' + +// Constants for noun type directories +const PERSON_DIR = 'person' +const PLACE_DIR = 'place' +const THING_DIR = 'thing' +const EVENT_DIR = 'event' +const CONCEPT_DIR = 'concept' +const CONTENT_DIR = 'content' +const DEFAULT_DIR = 'default' // For nodes without a noun type + +export class OPFSStorage implements StorageAdapter { + private rootDir: FileSystemDirectoryHandle | null = null + private nounsDir: FileSystemDirectoryHandle | null = null + private verbsDir: FileSystemDirectoryHandle | null = null + private metadataDir: FileSystemDirectoryHandle | null = null + private personDir: FileSystemDirectoryHandle | null = null + private placeDir: FileSystemDirectoryHandle | null = null + private thingDir: FileSystemDirectoryHandle | null = null + private eventDir: FileSystemDirectoryHandle | null = null + private conceptDir: FileSystemDirectoryHandle | null = null + private contentDir: FileSystemDirectoryHandle | null = null + private defaultDir: FileSystemDirectoryHandle | null = null + private isInitialized = false + private isAvailable = false + private isPersistentRequested = false + private isPersistentGranted = false + + constructor() { + // Check if OPFS is available + this.isAvailable = + typeof navigator !== 'undefined' && + 'storage' in navigator && + 'getDirectory' in navigator.storage + } + + /** + * Initialize the storage adapter + */ + public async init(): Promise { + if (this.isInitialized) { + return + } + + if (!this.isAvailable) { + throw new Error( + 'Origin Private File System is not available in this environment' + ) + } + + try { + // Get the root directory + const root = await navigator.storage.getDirectory() + + // Create or get our app's root directory + this.rootDir = await root.getDirectoryHandle(ROOT_DIR, { create: true }) + + // Create or get nouns directory + this.nounsDir = await this.rootDir.getDirectoryHandle(NOUNS_DIR, { + create: true + }) + + // Create or get verbs directory + this.verbsDir = await this.rootDir.getDirectoryHandle(VERBS_DIR, { + create: true + }) + + // Create or get metadata directory + this.metadataDir = await this.rootDir.getDirectoryHandle(METADATA_DIR, { + create: true + }) + + // Create or get noun type directories + this.personDir = await this.nounsDir.getDirectoryHandle(PERSON_DIR, { + create: true + }) + this.placeDir = await this.nounsDir.getDirectoryHandle(PLACE_DIR, { + create: true + }) + this.thingDir = await this.nounsDir.getDirectoryHandle(THING_DIR, { + create: true + }) + this.eventDir = await this.nounsDir.getDirectoryHandle(EVENT_DIR, { + create: true + }) + this.conceptDir = await this.nounsDir.getDirectoryHandle(CONCEPT_DIR, { + create: true + }) + this.contentDir = await this.nounsDir.getDirectoryHandle(CONTENT_DIR, { + create: true + }) + this.defaultDir = await this.nounsDir.getDirectoryHandle(DEFAULT_DIR, { + create: true + }) + + this.isInitialized = true + } catch (error) { + console.error('Failed to initialize OPFS storage:', error) + throw new Error(`Failed to initialize OPFS storage: ${error}`) + } + } + + /** + * Check if OPFS is available in the current environment + */ + public isOPFSAvailable(): boolean { + return this.isAvailable + } + + /** + * Request persistent storage permission from the user + * @returns Promise that resolves to true if permission was granted, false otherwise + */ + public async requestPersistentStorage(): Promise { + if (!this.isAvailable) { + console.warn('Cannot request persistent storage: OPFS is not available') + return false + } + + try { + // Check if persistence is already granted + this.isPersistentGranted = await navigator.storage.persisted() + + if (!this.isPersistentGranted) { + // Request permission for persistent storage + this.isPersistentGranted = await navigator.storage.persist() + } + + this.isPersistentRequested = true + return this.isPersistentGranted + } catch (error) { + console.warn('Failed to request persistent storage:', error) + return false + } + } + + /** + * Check if persistent storage is granted + * @returns Promise that resolves to true if persistent storage is granted, false otherwise + */ + public async isPersistent(): Promise { + if (!this.isAvailable) { + return false + } + + try { + this.isPersistentGranted = await navigator.storage.persisted() + return this.isPersistentGranted + } catch (error) { + console.warn('Failed to check persistent storage status:', error) + return false + } + } + + /** + * Save a noun to storage + */ + public async saveNoun(noun: HNSWNoun): Promise { + await this.ensureInitialized() + + try { + // Convert connections Map to a serializable format + const serializableNoun = { + ...noun, + connections: this.mapToObject(noun.connections, (set) => + Array.from(set as Set) + ) + } + + // Get the appropriate directory based on the noun's metadata + const nounDir = await this.getNodeDirectory(noun.id) + + // Create or get the file for this noun + const fileHandle = await nounDir.getFileHandle(noun.id, { + create: true + }) + + // Write the noun data to the file + const writable = await fileHandle.createWritable() + await writable.write(JSON.stringify(serializableNoun)) + await writable.close() + } catch (error) { + console.error(`Failed to save noun ${noun.id}:`, error) + throw new Error(`Failed to save noun ${noun.id}: ${error}`) + } + } + + /** + * Get a noun from storage + */ + public async getNoun(id: string): Promise { + await this.ensureInitialized() + + try { + // Get the appropriate directory based on the node's metadata + const nodeDir = await this.getNodeDirectory(id) + + try { + // Get the file handle for this node + const fileHandle = await nodeDir.getFileHandle(id) + + // Read the node data from the file + const file = await fileHandle.getFile() + const text = await file.text() + const data = JSON.parse(text) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(data.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: data.id, + vector: data.vector, + connections + } + } catch (dirError) { + // If the file doesn't exist in the expected directory, try the default directory + if (nodeDir !== this.defaultDir) { + try { + // Get the file handle from the default directory + const fileHandle = await this.defaultDir!.getFileHandle(id) + + // Read the node data from the file + const file = await fileHandle.getFile() + const text = await file.text() + const data = JSON.parse(text) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(data.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: data.id, + vector: data.vector, + connections + } + } catch (defaultDirError) { + // If not found in default directory either, try all noun type directories + const directories = [ + this.personDir!, + this.placeDir!, + this.thingDir!, + this.eventDir!, + this.conceptDir!, + this.contentDir! + ] + + for (const dir of directories) { + if (dir === nodeDir) continue // Skip the already checked directory + + try { + // Get the file handle from this directory + const fileHandle = await dir.getFileHandle(id) + + // Read the node data from the file + const file = await fileHandle.getFile() + const text = await file.text() + const data = JSON.parse(text) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries( + data.connections + )) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: data.id, + vector: data.vector, + connections + } + } catch { + // Continue to the next directory + } + } + + return null // File doesn't exist in any directory + } + } + return null // File doesn't exist + } + } catch (error) { + console.error(`Failed to get node ${id}:`, error) + throw new Error(`Failed to get node ${id}: ${error}`) + } + } + + /** + * Get nouns by noun type + * @param nounType The noun type to filter by + * @returns Promise that resolves to an array of nouns of the specified noun type + */ + public async getNounsByNounType(nounType: string): Promise { + await this.ensureInitialized() + + try { + const nouns: HNSWNoun[] = [] + + // Determine the directory based on the noun type + let dir: FileSystemDirectoryHandle + switch (nounType) { + case 'person': + dir = this.personDir! + break + case 'place': + dir = this.placeDir! + break + case 'thing': + dir = this.thingDir! + break + case 'event': + dir = this.eventDir! + break + case 'concept': + dir = this.conceptDir! + break + case 'content': + dir = this.contentDir! + break + default: + dir = this.defaultDir! + } + + try { + // Get all entries (filename and handle pairs) in this directory + const entries = dir.entries() + + // Iterate through all entries and get the corresponding nodes + for await (const [name, handle] of entries) { + try { + // The handle is already a FileSystemHandle, but we need to ensure it's a file + if (handle.kind !== 'file') continue + + // Cast to FileSystemFileHandle + const fileHandle = handle as FileSystemFileHandle + + // Read the node data from the file + const file = await fileHandle.getFile() + const text = await file.text() + const data = JSON.parse(text) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(data.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + nouns.push({ + id: data.id, + vector: data.vector, + connections + }) + } catch (nodeError) { + console.warn( + `Failed to read node ${name} from directory:`, + nodeError + ) + // Continue to the next node + } + } + } catch (dirError) { + console.warn( + `Failed to read directory for noun type ${nounType}:`, + dirError + ) + } + + return nouns + } catch (error) { + console.error(`Failed to get nouns for noun type ${nounType}:`, error) + throw new Error(`Failed to get nouns for noun type ${nounType}: ${error}`) + } + } + + /** + * Get all nouns from storage + */ + public async getAllNouns(): Promise { + await this.ensureInitialized() + + try { + // Get all noun types + const nounTypes = [ + 'person', + 'place', + 'thing', + 'event', + 'concept', + 'content', + 'default' + ] + + // Run searches in parallel for all noun types + const nounPromises = nounTypes.map((nounType) => + this.getNounsByNounType(nounType) + ) + const nounArrays = await Promise.all(nounPromises) + + // Combine all results + const allNouns: HNSWNoun[] = [] + for (const nouns of nounArrays) { + allNouns.push(...nouns) + } + + return allNouns + } catch (error) { + console.error('Failed to get all nouns:', error) + throw new Error(`Failed to get all nouns: ${error}`) + } + } + + /** + * Delete a noun from storage + */ + public async deleteNoun(id: string): Promise { + await this.ensureInitialized() + + try { + // Get the appropriate directory based on the noun's metadata + const nounDir = await this.getNodeDirectory(id) + + try { + // Try to delete the noun from the appropriate directory + await nounDir.removeEntry(id) + return // Noun deleted successfully + } catch (dirError) { + // If the file doesn't exist in the expected directory, try the default directory + if (nounDir !== this.defaultDir) { + try { + await this.defaultDir!.removeEntry(id) + return // Noun deleted successfully + } catch (defaultDirError) { + // If not found in default directory either, try all noun type directories + const directories = [ + this.personDir!, + this.placeDir!, + this.thingDir!, + this.eventDir!, + this.conceptDir!, + this.contentDir! + ] + + for (const dir of directories) { + if (dir === nounDir) continue // Skip the already checked directory + + try { + await dir.removeEntry(id) + return // Node deleted successfully + } catch { + // Continue to the next directory + } + } + + // If we get here, the node wasn't found in any directory + return + } + } + // If the file doesn't exist, that's fine + return + } + } catch (error) { + console.error(`Failed to delete noun ${id}:`, error) + throw new Error(`Failed to delete noun ${id}: ${error}`) + } + } + + /** + * Save a verb to storage + */ + public async saveVerb(verb: GraphVerb): Promise { + await this.ensureInitialized() + + try { + // Convert connections Map to a serializable format + const serializableVerb = { + ...verb, + connections: this.mapToObject(verb.connections, (set) => + Array.from(set as Set) + ) + } + + // Create or get the file for this verb + const fileHandle = await this.verbsDir!.getFileHandle(verb.id, { + create: true + }) + + // Write the verb data to the file + const writable = await fileHandle.createWritable() + await writable.write(JSON.stringify(serializableVerb)) + await writable.close() + } catch (error) { + console.error(`Failed to save verb ${verb.id}:`, error) + throw new Error(`Failed to save verb ${verb.id}: ${error}`) + } + } + + /** + * Get a verb from storage + */ + public async getVerb(id: string): Promise { + await this.ensureInitialized() + + try { + // Get the file handle for this verb + const fileHandle = await this.verbsDir!.getFileHandle(id) + + // Read the verb data from the file + const file = await fileHandle.getFile() + const text = await file.text() + const data = JSON.parse(text) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(data.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: data.id, + vector: data.vector, + connections, + sourceId: data.sourceId, + targetId: data.targetId, + type: data.type, + weight: data.weight, + metadata: data.metadata + } + } catch (error) { + // If the file doesn't exist, return null + if ((error as any).name === 'NotFoundError') { + return null + } + + console.error(`Failed to get verb ${id}:`, error) + throw new Error(`Failed to get verb ${id}: ${error}`) + } + } + + /** + * Get all edges from storage + */ + public async getAllEdges(): Promise { + await this.ensureInitialized() + + try { + const edges: Edge[] = [] + + // Get all entries (filename and handle pairs) in the verbs directory + const entries = this.verbsDir!.entries() + + // Iterate through all entries and get the corresponding edges + for await (const [name, handle] of entries) { + // Skip if not a file + if (handle.kind !== 'file') continue + + const edge = await this.getVerb(name) + if (edge) { + edges.push(edge) + } + } + + return edges + } catch (error) { + console.error('Failed to get all edges:', error) + throw new Error(`Failed to get all edges: ${error}`) + } + } + + /** + * Get all verbs from storage (alias for getAllEdges) + */ + public async getAllVerbs(): Promise { + return this.getAllEdges() + } + + /** + * Delete an edge from storage + */ + public async deleteEdge(id: string): Promise { + await this.ensureInitialized() + + try { + await this.verbsDir!.removeEntry(id) + } catch (error) { + // Ignore if the file doesn't exist + if ((error as any).name !== 'NotFoundError') { + console.error(`Failed to delete edge ${id}:`, error) + throw new Error(`Failed to delete edge ${id}: ${error}`) + } + } + } + + /** + * Delete a verb from storage (alias for deleteEdge) + */ + public async deleteVerb(id: string): Promise { + return this.deleteEdge(id) + } + + /** + * Get edges by source node ID + */ + public async getEdgesBySource(sourceId: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllEdges() + return allEdges.filter((edge) => edge.sourceId === sourceId) + } catch (error) { + console.error(`Failed to get edges by source ${sourceId}:`, error) + throw new Error(`Failed to get edges by source ${sourceId}: ${error}`) + } + } + + /** + * Get verbs by source node ID (alias for getEdgesBySource) + */ + public async getVerbsBySource(sourceId: string): Promise { + return this.getEdgesBySource(sourceId) + } + + /** + * Get edges by target node ID + */ + public async getEdgesByTarget(targetId: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllEdges() + return allEdges.filter((edge) => edge.targetId === targetId) + } catch (error) { + console.error(`Failed to get edges by target ${targetId}:`, error) + throw new Error(`Failed to get edges by target ${targetId}: ${error}`) + } + } + + /** + * Get verbs by target node ID (alias for getEdgesByTarget) + */ + public async getVerbsByTarget(targetId: string): Promise { + return this.getEdgesByTarget(targetId) + } + + /** + * Get edges by type + */ + public async getEdgesByType(type: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllEdges() + return allEdges.filter((edge) => edge.type === type) + } catch (error) { + console.error(`Failed to get edges by type ${type}:`, error) + throw new Error(`Failed to get edges by type ${type}: ${error}`) + } + } + + /** + * Get verbs by type (alias for getEdgesByType) + */ + public async getVerbsByType(type: string): Promise { + return this.getEdgesByType(type) + } + + /** + * Save metadata for a node + */ + public async saveMetadata(id: string, metadata: any): Promise { + await this.ensureInitialized() + + try { + // Create or get the file for this metadata + const fileHandle = await this.metadataDir!.getFileHandle(id, { + create: true + }) + + // Write the metadata to the file + const writable = await fileHandle.createWritable() + await writable.write(JSON.stringify(metadata)) + await writable.close() + } catch (error) { + console.error(`Failed to save metadata for ${id}:`, error) + throw new Error(`Failed to save metadata for ${id}: ${error}`) + } + } + + /** + * Get metadata for a node + */ + public async getMetadata(id: string): Promise { + await this.ensureInitialized() + + try { + // Get the file handle for this metadata + const fileHandle = await this.metadataDir!.getFileHandle(id) + + // Read the metadata from the file + const file = await fileHandle.getFile() + const text = await file.text() + return JSON.parse(text) + } catch (error) { + // If the file doesn't exist, return null + if ((error as any).name === 'NotFoundError') { + return null + } + + console.error(`Failed to get metadata for ${id}:`, error) + throw new Error(`Failed to get metadata for ${id}: ${error}`) + } + } + + /** + * Clear all data from storage + */ + public async clear(): Promise { + await this.ensureInitialized() + + try { + // Delete and recreate the nouns directory + await this.rootDir!.removeEntry(NOUNS_DIR, { recursive: true }) + this.nounsDir = await this.rootDir!.getDirectoryHandle(NOUNS_DIR, { + create: true + }) + + // Create noun type directories + this.personDir = await this.nounsDir.getDirectoryHandle(PERSON_DIR, { + create: true + }) + this.placeDir = await this.nounsDir.getDirectoryHandle(PLACE_DIR, { + create: true + }) + this.thingDir = await this.nounsDir.getDirectoryHandle(THING_DIR, { + create: true + }) + this.eventDir = await this.nounsDir.getDirectoryHandle(EVENT_DIR, { + create: true + }) + this.conceptDir = await this.nounsDir.getDirectoryHandle(CONCEPT_DIR, { + create: true + }) + this.contentDir = await this.nounsDir.getDirectoryHandle(CONTENT_DIR, { + create: true + }) + this.defaultDir = await this.nounsDir.getDirectoryHandle(DEFAULT_DIR, { + create: true + }) + + // Delete and recreate the verbs directory + await this.rootDir!.removeEntry(VERBS_DIR, { recursive: true }) + this.verbsDir = await this.rootDir!.getDirectoryHandle(VERBS_DIR, { + create: true + }) + + // Delete and recreate the metadata directory + await this.rootDir!.removeEntry(METADATA_DIR, { recursive: true }) + this.metadataDir = await this.rootDir!.getDirectoryHandle(METADATA_DIR, { + create: true + }) + } catch (error) { + console.error('Failed to clear storage:', error) + throw new Error(`Failed to clear storage: ${error}`) + } + } + + /** + * Ensure the storage adapter is initialized + */ + private async ensureInitialized(): Promise { + if (!this.isInitialized) { + await this.init() + } + } + + /** + * Convert a Map to a plain object for serialization + */ + private mapToObject( + map: Map, + valueTransformer: (value: V) => any = (v) => v + ): Record { + const obj: Record = {} + for (const [key, value] of map.entries()) { + obj[key.toString()] = valueTransformer(value) + } + return obj + } + + /** + * Get the appropriate directory for a node based on its metadata + */ + private async getNodeDirectory( + id: string + ): Promise { + try { + // Try to get the metadata for the node + const metadata = await this.getMetadata(id) + + // If metadata exists and has a noun field, use the corresponding directory + if (metadata && metadata.noun) { + switch (metadata.noun) { + case 'person': + return this.personDir! + case 'place': + return this.placeDir! + case 'thing': + return this.thingDir! + case 'event': + return this.eventDir! + case 'concept': + return this.conceptDir! + case 'content': + return this.contentDir! + default: + return this.defaultDir! + } + } + + // If no metadata or no noun field, use the default directory + return this.defaultDir! + } catch (error) { + // If there's an error getting the metadata, use the default directory + return this.defaultDir! + } + } + + /** + * Get information about storage usage and capacity + */ + public async getStorageStatus(): Promise<{ + type: string + used: number + quota: number | null + details?: Record + }> { + await this.ensureInitialized() + + try { + // Calculate the total size of all files in the storage directories + let totalSize = 0 + + // Helper function to calculate directory size + const calculateDirSize = async ( + dirHandle: FileSystemDirectoryHandle + ): Promise => { + let size = 0 + try { + for await (const [name, handle] of dirHandle.entries()) { + if (handle.kind === 'file') { + const file = await (handle as FileSystemFileHandle).getFile() + size += file.size + } else if (handle.kind === 'directory') { + size += await calculateDirSize( + handle as FileSystemDirectoryHandle + ) + } + } + } catch (error) { + console.warn(`Error calculating size for directory:`, error) + } + return size + } + + // Helper function to count files in a directory + const countFilesInDirectory = async ( + dirHandle: FileSystemDirectoryHandle + ): Promise => { + let count = 0 + try { + for await (const [name, handle] of dirHandle.entries()) { + if (handle.kind === 'file') { + count++ + } + } + } catch (error) { + console.warn(`Error counting files in directory:`, error) + } + return count + } + + // Calculate size for each directory + if (this.nounsDir) { + totalSize += await calculateDirSize(this.nounsDir) + } + if (this.verbsDir) { + totalSize += await calculateDirSize(this.verbsDir) + } + if (this.metadataDir) { + totalSize += await calculateDirSize(this.metadataDir) + } + + // Calculate sizes of noun type directories + const personDirSize = this.personDir + ? await calculateDirSize(this.personDir) + : 0 + const placeDirSize = this.placeDir + ? await calculateDirSize(this.placeDir) + : 0 + const thingDirSize = this.thingDir + ? await calculateDirSize(this.thingDir) + : 0 + const eventDirSize = this.eventDir + ? await calculateDirSize(this.eventDir) + : 0 + const conceptDirSize = this.conceptDir + ? await calculateDirSize(this.conceptDir) + : 0 + const contentDirSize = this.contentDir + ? await calculateDirSize(this.contentDir) + : 0 + const defaultDirSize = this.defaultDir + ? await calculateDirSize(this.defaultDir) + : 0 + + // Get storage quota information using the Storage API + let quota = null + let details: Record = { + isPersistent: await this.isPersistent(), + nounTypes: { + person: { + size: personDirSize, + count: this.personDir + ? await countFilesInDirectory(this.personDir) + : 0 + }, + place: { + size: placeDirSize, + count: this.placeDir + ? await countFilesInDirectory(this.placeDir) + : 0 + }, + thing: { + size: thingDirSize, + count: this.thingDir + ? await countFilesInDirectory(this.thingDir) + : 0 + }, + event: { + size: eventDirSize, + count: this.eventDir + ? await countFilesInDirectory(this.eventDir) + : 0 + }, + concept: { + size: conceptDirSize, + count: this.conceptDir + ? await countFilesInDirectory(this.conceptDir) + : 0 + }, + content: { + size: contentDirSize, + count: this.contentDir + ? await countFilesInDirectory(this.contentDir) + : 0 + }, + default: { + size: defaultDirSize, + count: this.defaultDir + ? await countFilesInDirectory(this.defaultDir) + : 0 + } + } + } + + try { + if (navigator.storage && navigator.storage.estimate) { + const estimate = await navigator.storage.estimate() + quota = estimate.quota || null + details = { + ...details, + usage: estimate.usage, + quota: estimate.quota, + freePercentage: estimate.quota + ? ((estimate.quota - (estimate.usage || 0)) / estimate.quota) * + 100 + : null + } + } + } catch (error) { + console.warn('Unable to get storage estimate:', error) + } + + return { + type: 'opfs', + used: totalSize, + quota, + details + } + } catch (error) { + console.error('Failed to get storage status:', error) + return { + type: 'opfs', + used: 0, + quota: null, + details: { error: String(error) } + } + } + } +} + +/** + * In-memory storage adapter for environments where OPFS is not available + */ +export class MemoryStorage implements StorageAdapter { + // Map of noun type to Map of noun ID to noun + private nouns: Map> = new Map() + private verbs: Map = new Map() + private metadata: Map = new Map() + + // Initialize maps for each noun type + constructor() { + this.nouns.set(PERSON_DIR, new Map()) + this.nouns.set(PLACE_DIR, new Map()) + this.nouns.set(THING_DIR, new Map()) + this.nouns.set(EVENT_DIR, new Map()) + this.nouns.set(CONCEPT_DIR, new Map()) + this.nouns.set(CONTENT_DIR, new Map()) + this.nouns.set(DEFAULT_DIR, new Map()) + } + + // Alias methods to match StorageAdapter interface + public async saveNoun(noun: HNSWNoun): Promise { + return this.saveNode(noun) + } + + public async getNoun(id: string): Promise { + return this.getNode(id) + } + + public async getAllNouns(): Promise { + return this.getAllNodes() + } + + public async getNounsByNounType(nounType: string): Promise { + return this.getNodesByNounType(nounType) + } + + public async deleteNoun(id: string): Promise { + return this.deleteNode(id) + } + + public async saveVerb(verb: GraphVerb): Promise { + return this.saveEdge(verb) + } + + public async getVerb(id: string): Promise { + return this.getEdge(id) + } + + public async getAllVerbs(): Promise { + return this.getAllEdges() + } + + public async getVerbsBySource(sourceId: string): Promise { + return this.getEdgesBySource(sourceId) + } + + public async getVerbsByTarget(targetId: string): Promise { + return this.getEdgesByTarget(targetId) + } + + public async getVerbsByType(type: string): Promise { + return this.getEdgesByType(type) + } + + public async deleteVerb(id: string): Promise { + return this.deleteEdge(id) + } + + public async init(): Promise { + // Nothing to initialize for in-memory storage + } + + /** + * Get the appropriate node type for a node based on its metadata + */ + private async getNodeType(id: string): Promise { + try { + // Try to get the metadata for the node + const metadata = await this.getMetadata(id) + + // If metadata exists and has a noun field, use the corresponding type + if (metadata && metadata.noun) { + switch (metadata.noun) { + case 'person': + return PERSON_DIR + case 'place': + return PLACE_DIR + case 'thing': + return THING_DIR + case 'event': + return EVENT_DIR + case 'concept': + return CONCEPT_DIR + case 'content': + return CONTENT_DIR + default: + return DEFAULT_DIR + } + } + + // If no metadata or no noun field, use the default type + return DEFAULT_DIR + } catch (error) { + // If there's an error getting the metadata, use the default type + return DEFAULT_DIR + } + } + + public async saveNode(node: HNSWNode): Promise { + // Create a deep copy to avoid reference issues + const nodeCopy: HNSWNode = { + id: node.id, + vector: [...node.vector], + connections: new Map() + } + + // Copy connections + for (const [level, connections] of node.connections.entries()) { + nodeCopy.connections.set(level, new Set(connections)) + } + + // Get the appropriate node type based on the node's metadata + const nodeType = await this.getNodeType(node.id) + + // Get the map for this node type + const nodeMap = this.nouns.get(nodeType)! + + // Save the node in the appropriate map + nodeMap.set(node.id, nodeCopy) + } + + public async getNode(id: string): Promise { + // Get the appropriate node type based on the node's metadata + const nodeType = await this.getNodeType(id) + + // Get the map for this node type + const nodeMap = this.nouns.get(nodeType)! + + // Try to get the node from the appropriate map + let node = nodeMap.get(id) + + // If not found in the expected map, try other maps + if (!node) { + // If the node type is not the default type, try the default map + if (nodeType !== DEFAULT_DIR) { + const defaultMap = this.nouns.get(DEFAULT_DIR)! + node = defaultMap.get(id) + + // If still not found, try all other maps + if (!node) { + const nodeTypes = [ + PERSON_DIR, + PLACE_DIR, + THING_DIR, + EVENT_DIR, + CONCEPT_DIR, + CONTENT_DIR + ] + for (const type of nodeTypes) { + if (type === nodeType) continue // Skip the already checked map + + const typeMap = this.nouns.get(type)! + node = typeMap.get(id) + if (node) break // Found the node, exit the loop + } + } + } + + // If still not found, return null + if (!node) { + return null + } + } + + // Return a deep copy to avoid reference issues + const nodeCopy: HNSWNode = { + id: node.id, + vector: [...node.vector], + connections: new Map() + } + + // Copy connections + for (const [level, connections] of node.connections.entries()) { + nodeCopy.connections.set(level, new Set(connections)) + } + + return nodeCopy + } + + /** + * Get nodes by noun type + * @param nounType The noun type to filter by + * @returns Promise that resolves to an array of nodes of the specified noun type + */ + public async getNodesByNounType(nounType: string): Promise { + const nodes: HNSWNode[] = [] + + // Get the map for this noun type + let typeMap: Map + switch (nounType) { + case 'person': + typeMap = this.nouns.get(PERSON_DIR)! + break + case 'place': + typeMap = this.nouns.get(PLACE_DIR)! + break + case 'thing': + typeMap = this.nouns.get(THING_DIR)! + break + case 'event': + typeMap = this.nouns.get(EVENT_DIR)! + break + case 'concept': + typeMap = this.nouns.get(CONCEPT_DIR)! + break + case 'content': + typeMap = this.nouns.get(CONTENT_DIR)! + break + default: + typeMap = this.nouns.get(DEFAULT_DIR)! + } + + // Get all nodes from this map + for (const nodeId of typeMap.keys()) { + const node = await this.getNode(nodeId) + if (node) { + nodes.push(node) + } + } + + return nodes + } + + public async getAllNodes(): Promise { + // Get all noun types + const nounTypes = [ + 'person', + 'place', + 'thing', + 'event', + 'concept', + 'content', + 'default' + ] + + // Run searches in parallel for all noun types + const nodePromises = nounTypes.map((nounType) => + this.getNodesByNounType(nounType) + ) + const nodeArrays = await Promise.all(nodePromises) + + // Combine all results + const allNodes: HNSWNode[] = [] + for (const nodes of nodeArrays) { + allNodes.push(...nodes) + } + + return allNodes + } + + public async deleteNode(id: string): Promise { + // Get the appropriate node type based on the node's metadata + const nodeType = await this.getNodeType(id) + + // Get the map for this node type + const nodeMap = this.nouns.get(nodeType)! + + // Try to delete the node from the appropriate map + const deleted = nodeMap.delete(id) + + // If not found in the expected map, try other maps + if (!deleted) { + // If the node type is not the default type, try the default map + if (nodeType !== DEFAULT_DIR) { + const defaultMap = this.nouns.get(DEFAULT_DIR)! + const deletedFromDefault = defaultMap.delete(id) + + // If still not found, try all other maps + if (!deletedFromDefault) { + const nodeTypes = [ + PERSON_DIR, + PLACE_DIR, + THING_DIR, + EVENT_DIR, + CONCEPT_DIR, + CONTENT_DIR + ] + for (const type of nodeTypes) { + if (type === nodeType) continue // Skip the already checked map + + const typeMap = this.nouns.get(type)! + const deletedFromType = typeMap.delete(id) + if (deletedFromType) break // Node deleted, exit the loop + } + } + } + } + } + + public async saveMetadata(id: string, metadata: any): Promise { + this.metadata.set(id, JSON.parse(JSON.stringify(metadata))) + } + + public async getMetadata(id: string): Promise { + const metadata = this.metadata.get(id) + if (!metadata) { + return null + } + + return JSON.parse(JSON.stringify(metadata)) + } + + public async saveEdge(edge: Edge): Promise { + // Create a deep copy to avoid reference issues + const edgeCopy: Edge = { + id: edge.id, + vector: [...edge.vector], + connections: new Map(), + sourceId: edge.sourceId, + targetId: edge.targetId, + type: edge.type, + weight: edge.weight, + metadata: edge.metadata + ? JSON.parse(JSON.stringify(edge.metadata)) + : undefined + } + + // Copy connections + for (const [level, connections] of edge.connections.entries()) { + edgeCopy.connections.set(level, new Set(connections)) + } + + this.verbs.set(edge.id, edgeCopy) + } + + public async getEdge(id: string): Promise { + const edge = this.verbs.get(id) + if (!edge) { + return null + } + + // Return a deep copy to avoid reference issues + const edgeCopy: Edge = { + id: edge.id, + vector: [...edge.vector], + connections: new Map(), + sourceId: edge.sourceId, + targetId: edge.targetId, + type: edge.type, + weight: edge.weight, + metadata: edge.metadata + ? JSON.parse(JSON.stringify(edge.metadata)) + : undefined + } + + // Copy connections + for (const [level, connections] of edge.connections.entries()) { + edgeCopy.connections.set(level, new Set(connections)) + } + + return edgeCopy + } + + public async getAllEdges(): Promise { + const edges: Edge[] = [] + + for (const edgeId of this.verbs.keys()) { + const edge = await this.getEdge(edgeId) + if (edge) { + edges.push(edge) + } + } + + return edges + } + + public async getEdgesBySource(sourceId: string): Promise { + const edges: Edge[] = [] + + for (const edge of this.verbs.values()) { + if (edge.sourceId === sourceId) { + const edgeCopy = await this.getEdge(edge.id) + if (edgeCopy) { + edges.push(edgeCopy) + } + } + } + + return edges + } + + public async getEdgesByTarget(targetId: string): Promise { + const edges: Edge[] = [] + + for (const edge of this.verbs.values()) { + if (edge.targetId === targetId) { + const edgeCopy = await this.getEdge(edge.id) + if (edgeCopy) { + edges.push(edgeCopy) + } + } + } + + return edges + } + + public async getEdgesByType(type: string): Promise { + const edges: Edge[] = [] + + for (const edge of this.verbs.values()) { + if (edge.type === type) { + const edgeCopy = await this.getEdge(edge.id) + if (edgeCopy) { + edges.push(edgeCopy) + } + } + } + + return edges + } + + public async deleteEdge(id: string): Promise { + this.verbs.delete(id) + } + + public async clear(): Promise { + // Clear all noun type maps + const nodeTypes = [ + PERSON_DIR, + PLACE_DIR, + THING_DIR, + EVENT_DIR, + CONCEPT_DIR, + CONTENT_DIR, + DEFAULT_DIR + ] + for (const type of nodeTypes) { + const typeMap = this.nouns.get(type)! + typeMap.clear() + } + + this.verbs.clear() + this.metadata.clear() + } + + /** + * Get information about storage usage and capacity + */ + public async getStorageStatus(): Promise<{ + type: string + used: number + quota: number | null + details?: Record + }> { + try { + // Estimate the size of data in memory + let totalSize = 0 + + // Helper function to estimate object size in bytes + const estimateSize = (obj: any): number => { + if (obj === null || obj === undefined) return 0 + + const type = typeof obj + + // Handle primitive types + if (type === 'number') return 8 + if (type === 'string') return obj.length * 2 + if (type === 'boolean') return 4 + + // Handle arrays and objects + if (Array.isArray(obj)) { + return obj.reduce((size, item) => size + estimateSize(item), 0) + } + + if (type === 'object') { + if (obj instanceof Map) { + let mapSize = 0 + for (const [key, value] of obj.entries()) { + mapSize += estimateSize(key) + estimateSize(value) + } + return mapSize + } + + if (obj instanceof Set) { + let setSize = 0 + for (const item of obj) { + setSize += estimateSize(item) + } + return setSize + } + + // Regular object + return Object.entries(obj).reduce( + (size, [key, value]) => size + key.length * 2 + estimateSize(value), + 0 + ) + } + + return 0 + } + + // Calculate sizes and counts for each noun type + const nounTypeDetails: Record = + {} + const nodeTypes = [ + PERSON_DIR, + PLACE_DIR, + THING_DIR, + EVENT_DIR, + CONCEPT_DIR, + CONTENT_DIR, + DEFAULT_DIR + ] + + let totalNodeCount = 0 + let nodesSize = 0 + + for (const type of nodeTypes) { + const typeMap = this.nouns.get(type)! + let typeSize = 0 + + for (const node of typeMap.values()) { + typeSize += estimateSize(node) + } + + nounTypeDetails[type] = { + size: typeSize, + count: typeMap.size + } + + nodesSize += typeSize + totalNodeCount += typeMap.size + } + + totalSize += nodesSize + + // Estimate size of edges + let edgesSize = 0 + for (const edge of this.verbs.values()) { + edgesSize += estimateSize(edge) + } + totalSize += edgesSize + + // Estimate size of metadata + let metadataSize = 0 + for (const meta of this.metadata.values()) { + metadataSize += estimateSize(meta) + } + totalSize += metadataSize + + // Get memory information if available + let quota = null + let details: Record = { + nodeCount: totalNodeCount, + edgeCount: this.verbs.size, + metadataCount: this.metadata.size, + nounTypes: nounTypeDetails, + nodesSize, + edgesSize, + metadataSize + } + + // Try to get memory information if in a browser environment + if ( + typeof window !== 'undefined' && + (window as any).performance && + (window as any).performance.memory + ) { + const memory = (window as any).performance.memory + quota = memory.jsHeapSizeLimit + details = { + ...details, + totalJSHeapSize: memory.totalJSHeapSize, + usedJSHeapSize: memory.usedJSHeapSize, + jsHeapSizeLimit: memory.jsHeapSizeLimit + } + } + + return { + type: 'memory', + used: totalSize, + quota, + details + } + } catch (error) { + console.error('Failed to get memory storage status:', error) + return { + type: 'memory', + used: 0, + quota: null, + details: { error: String(error) } + } + } + } +} + +/** + * Factory function to create the appropriate storage adapter based on the environment + * @param options Configuration options for storage + * @returns Promise that resolves to a StorageAdapter instance + */ +export async function createStorage( + options: { + requestPersistentStorage?: boolean + r2Storage?: { + bucketName?: string + accountId?: string + accessKeyId?: string + secretAccessKey?: string + } + s3Storage?: { + bucketName?: string + accessKeyId?: string + secretAccessKey?: string + region?: string + } + gcsStorage?: { + bucketName?: string + accessKeyId?: string + secretAccessKey?: string + endpoint?: string + } + customS3Storage?: { + bucketName?: string + accessKeyId?: string + secretAccessKey?: string + endpoint?: string + region?: string + } + forceFileSystemStorage?: boolean + forceMemoryStorage?: boolean + } = {} +): Promise { + // Detect environment + const environment = { + isBrowser: typeof window !== 'undefined', + isNode: + typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null, + isServerless: + typeof window === 'undefined' && + (typeof process === 'undefined' || + !process.versions || + !process.versions.node) + } + + // If force memory storage is specified, use MemoryStorage regardless of environment + if (options.forceMemoryStorage) { + console.log('Using in-memory storage (forced by configuration)') + return new MemoryStorage() + } + + // Default empty values for environment variables + const defaultEnvStorage = { + bucketName: undefined, + accountId: undefined, + accessKeyId: undefined, + secretAccessKey: undefined, + region: undefined, + endpoint: undefined + } + + // Try to use cloud storage if configured + if ( + options.r2Storage || + options.s3Storage || + options.gcsStorage || + options.customS3Storage || + (environment.isNode && + (process.env.R2_BUCKET_NAME || + process.env.S3_BUCKET_NAME || + process.env.GCS_BUCKET_NAME)) + ) { + try { + // Only try to access process.env in Node.js environment + const envR2Storage = environment.isNode + ? { + bucketName: process.env.R2_BUCKET_NAME, + accountId: process.env.R2_ACCOUNT_ID, + accessKeyId: process.env.R2_ACCESS_KEY_ID, + secretAccessKey: process.env.R2_SECRET_ACCESS_KEY + } + : defaultEnvStorage + + const envS3Storage = environment.isNode + ? { + bucketName: process.env.S3_BUCKET_NAME, + accessKeyId: + process.env.S3_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: + process.env.S3_SECRET_ACCESS_KEY || + process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.S3_REGION || process.env.AWS_REGION + } + : defaultEnvStorage + + const envGCSStorage = environment.isNode + ? { + bucketName: process.env.GCS_BUCKET_NAME, + accessKeyId: process.env.GCS_ACCESS_KEY_ID, + secretAccessKey: process.env.GCS_SECRET_ACCESS_KEY, + endpoint: process.env.GCS_ENDPOINT + } + : defaultEnvStorage + + // Merge environment variables with provided options + const mergedOptions = { + ...options, + r2Storage: options.r2Storage + ? { + ...envR2Storage, + ...options.r2Storage + } + : // Only use environment variables if they have the required fields + envR2Storage.bucketName && + envR2Storage.accessKeyId && + envR2Storage.secretAccessKey + ? envR2Storage + : undefined, + s3Storage: options.s3Storage + ? { + ...envS3Storage, + ...options.s3Storage + } + : // Only use environment variables if they have the required fields + envS3Storage.bucketName && + envS3Storage.accessKeyId && + envS3Storage.secretAccessKey + ? envS3Storage + : undefined, + gcsStorage: options.gcsStorage + ? { + ...envGCSStorage, + ...options.gcsStorage + } + : // Only use environment variables if they have the required fields + envGCSStorage.bucketName && + envGCSStorage.accessKeyId && + envGCSStorage.secretAccessKey + ? envGCSStorage + : undefined + } + + const s3Module = await import('./s3CompatibleStorage.js') + + if ( + mergedOptions.r2Storage && + mergedOptions.r2Storage.bucketName && + mergedOptions.r2Storage.accountId && + mergedOptions.r2Storage.accessKeyId && + mergedOptions.r2Storage.secretAccessKey + ) { + console.log('Using Cloudflare R2 storage') + return new s3Module.R2Storage({ + bucketName: mergedOptions.r2Storage.bucketName, + accountId: mergedOptions.r2Storage.accountId, + accessKeyId: mergedOptions.r2Storage.accessKeyId, + secretAccessKey: mergedOptions.r2Storage.secretAccessKey, + serviceType: 'r2' + }) + } else if ( + mergedOptions.s3Storage && + mergedOptions.s3Storage.bucketName && + mergedOptions.s3Storage.accessKeyId && + mergedOptions.s3Storage.secretAccessKey + ) { + console.log('Using Amazon S3 storage') + return new s3Module.S3CompatibleStorage({ + bucketName: mergedOptions.s3Storage.bucketName, + accessKeyId: mergedOptions.s3Storage.accessKeyId, + secretAccessKey: mergedOptions.s3Storage.secretAccessKey, + region: mergedOptions.s3Storage.region, + serviceType: 's3' + }) + } else if ( + mergedOptions.gcsStorage && + mergedOptions.gcsStorage.bucketName && + mergedOptions.gcsStorage.accessKeyId && + mergedOptions.gcsStorage.secretAccessKey + ) { + console.log('Using Google Cloud Storage') + return new s3Module.S3CompatibleStorage({ + bucketName: mergedOptions.gcsStorage.bucketName, + accessKeyId: mergedOptions.gcsStorage.accessKeyId, + secretAccessKey: mergedOptions.gcsStorage.secretAccessKey, + endpoint: mergedOptions.gcsStorage.endpoint, + serviceType: 'gcs' + }) + } else if ( + mergedOptions.customS3Storage && + mergedOptions.customS3Storage.bucketName && + mergedOptions.customS3Storage.accessKeyId && + mergedOptions.customS3Storage.secretAccessKey + ) { + console.log('Using custom S3-compatible storage') + return new s3Module.S3CompatibleStorage({ + bucketName: mergedOptions.customS3Storage.bucketName, + accessKeyId: mergedOptions.customS3Storage.accessKeyId, + secretAccessKey: mergedOptions.customS3Storage.secretAccessKey, + endpoint: mergedOptions.customS3Storage.endpoint, + region: mergedOptions.customS3Storage.region, + serviceType: 'custom' + }) + } + } catch (error) { + console.warn( + 'Failed to load S3CompatibleStorage, falling back to environment-specific storage:', + error + ) + // Continue to environment-specific storage selection + } + } + + // Environment-specific storage selection + if (environment.isBrowser) { + // In browser environments + if (!options.forceFileSystemStorage) { + try { + // Try OPFS first (Origin Private File System - browser-specific) + const opfsStorage = new OPFSStorage() + + if (opfsStorage.isOPFSAvailable()) { + // Request persistent storage if specified + if (options.requestPersistentStorage) { + const isPersistentGranted = + await opfsStorage.requestPersistentStorage() + if (isPersistentGranted) { + console.log('Persistent storage permission granted') + } else { + console.warn('Persistent storage permission denied') + } + } + console.log('Using Origin Private File System (OPFS) storage') + return opfsStorage + } + } catch (error) { + console.warn('OPFS storage initialization failed:', error) + } + } + + // Fall back to memory storage for browser environments + console.log('Using in-memory storage for browser environment') + return new MemoryStorage() + } else if (environment.isNode) { + // In Node.js environments + try { + const fileSystemModule = await import('./fileSystemStorage.js') + console.log('Using file system storage for Node.js environment') + return new fileSystemModule.FileSystemStorage() + } catch (error) { + console.warn('Failed to load FileSystemStorage:', error) + console.log('Using in-memory storage for Node.js environment') + return new MemoryStorage() + } + } else { + // In serverless or other environments + console.log('Using in-memory storage for serverless/unknown environment') + return new MemoryStorage() + } +} diff --git a/src/storage/s3CompatibleStorage.ts b/src/storage/s3CompatibleStorage.ts new file mode 100644 index 00000000..681827ca --- /dev/null +++ b/src/storage/s3CompatibleStorage.ts @@ -0,0 +1,1106 @@ +import { GraphVerb, HNSWNoun, StorageAdapter } from '../coreTypes.js' + +// Type aliases for compatibility +type HNSWNode = HNSWNoun +type Edge = GraphVerb + +// Export R2Storage as an alias for S3CompatibleStorage +export { S3CompatibleStorage as R2Storage } + +// Constants for S3 bucket prefixes +const NOUNS_PREFIX = 'nouns/' +const VERBS_PREFIX = 'verbs/' +const EDGES_PREFIX = 'verbs/' // Alias for VERBS_PREFIX for edge operations +const METADATA_PREFIX = 'metadata/' + +// Constants for noun type prefixes +const PERSON_PREFIX = 'nouns/person/' +const PLACE_PREFIX = 'place/' +const THING_PREFIX = 'thing/' +const EVENT_PREFIX = 'event/' +const CONCEPT_PREFIX = 'concept/' +const CONTENT_PREFIX = 'content/' +const DEFAULT_PREFIX = 'default/' // For nodes without a noun type + +/** + * S3-compatible storage adapter for server environments + * Uses the AWS S3 client to interact with S3-compatible storage services + * including Amazon S3, Cloudflare R2, and Google Cloud Storage + * + * To use this adapter with Cloudflare R2, you need to provide: + * - bucketName: Your bucket name + * - accountId: Your Cloudflare account ID + * - accessKeyId: R2 access key ID + * - secretAccessKey: R2 secret access key + * - serviceType: 'r2' + * + * To use this adapter with Amazon S3, you need to provide: + * - bucketName: Your S3 bucket name + * - accessKeyId: AWS access key ID + * - secretAccessKey: AWS secret access key + * - region: AWS region (e.g., 'us-east-1') + * - serviceType: 's3' + * + * To use this adapter with Google Cloud Storage, you need to provide: + * - bucketName: Your GCS bucket name + * - accessKeyId: HMAC access key + * - secretAccessKey: HMAC secret + * - endpoint: GCS endpoint (e.g., 'https://storage.googleapis.com') + * - serviceType: 'gcs' + * + * For other S3-compatible services, provide: + * - bucketName: Your bucket name + * - accessKeyId: Access key ID + * - secretAccessKey: Secret access key + * - endpoint: Service endpoint URL + * - region: Region (if required) + * - serviceType: 'custom' + */ +export class S3CompatibleStorage implements StorageAdapter { + private bucketName: string + private accessKeyId: string + private secretAccessKey: string + private endpoint?: string + private region?: string + private accountId?: string + private serviceType: 'r2' | 's3' | 'gcs' | 'custom' + private s3Client: any // Will be initialized in init() + private isInitialized = false + + // Alias methods to match StorageAdapter interface + public async saveNoun(noun: HNSWNoun): Promise { + return this.saveNode(noun) + } + + public async getNoun(id: string): Promise { + return this.getNode(id) + } + + public async getAllNouns(): Promise { + return this.getAllNodes() + } + + public async getNounsByNounType(nounType: string): Promise { + return this.getNodesByNounType(nounType) + } + + public async deleteNoun(id: string): Promise { + return this.deleteNode(id) + } + + public async saveVerb(verb: GraphVerb): Promise { + return this.saveEdge(verb) + } + + public async getVerb(id: string): Promise { + return this.getEdge(id) + } + + public async getAllVerbs(): Promise { + return this.getAllEdges() + } + + public async getVerbsBySource(sourceId: string): Promise { + return this.getEdgesBySource(sourceId) + } + + public async getVerbsByTarget(targetId: string): Promise { + return this.getEdgesByTarget(targetId) + } + + public async getVerbsByType(type: string): Promise { + return this.getEdgesByType(type) + } + + public async deleteVerb(id: string): Promise { + return this.deleteEdge(id) + } + + constructor(options: { + bucketName: string + accessKeyId: string + secretAccessKey: string + serviceType: 'r2' | 's3' | 'gcs' | 'custom' + accountId?: string + region?: string + endpoint?: string + }) { + this.bucketName = options.bucketName + this.accessKeyId = options.accessKeyId + this.secretAccessKey = options.secretAccessKey + this.serviceType = options.serviceType + this.accountId = options.accountId + this.region = options.region + this.endpoint = options.endpoint + } + + /** + * Initialize the storage adapter + */ + public async init(): Promise { + if (this.isInitialized) { + return + } + + try { + // Dynamically import the AWS SDK to avoid bundling it in browser environments + try { + const { S3Client } = await import('@aws-sdk/client-s3') + + // Configure the S3 client based on the service type + const clientConfig: any = { + credentials: { + accessKeyId: this.accessKeyId, + secretAccessKey: this.secretAccessKey + } + } + + switch (this.serviceType) { + case 'r2': + if (!this.accountId) { + throw new Error('accountId is required for Cloudflare R2') + } + clientConfig.region = 'auto' // R2 uses 'auto' as the region + clientConfig.endpoint = `https://${this.accountId}.r2.cloudflarestorage.com` + break + + case 's3': + if (!this.region) { + throw new Error('region is required for Amazon S3') + } + clientConfig.region = this.region + // No endpoint needed for standard S3 + break + + case 'gcs': + if (!this.endpoint) { + // Default GCS endpoint if not provided + this.endpoint = 'https://storage.googleapis.com' + } + clientConfig.endpoint = this.endpoint + clientConfig.region = this.region || 'auto' + break + + case 'custom': + if (!this.endpoint) { + throw new Error( + 'endpoint is required for custom S3-compatible services' + ) + } + clientConfig.endpoint = this.endpoint + if (this.region) { + clientConfig.region = this.region + } + break + + default: + throw new Error(`Unsupported service type: ${this.serviceType}`) + } + + // Initialize the S3 client with the configured options + this.s3Client = new S3Client(clientConfig) + + this.isInitialized = true + } catch (importError) { + throw new Error( + `Failed to import AWS SDK: ${importError}. Make sure @aws-sdk/client-s3 is installed.` + ) + } + } catch (error) { + console.error(`Failed to initialize ${this.serviceType} storage:`, error) + throw new Error( + `Failed to initialize ${this.serviceType} storage: ${error}` + ) + } + } + + /** + * Save a node to storage + */ + public async saveNode(node: HNSWNode): Promise { + await this.ensureInitialized() + + try { + // Convert connections Map to a serializable format + const serializableNode = { + ...node, + connections: this.mapToObject(node.connections, (set) => + Array.from(set as Set) + ) + } + + // Get the appropriate prefix based on the node's metadata + const nodePrefix = await this.getNodePrefix(node.id) + + // Import the PutObjectCommand only when needed + const { PutObjectCommand } = await import('@aws-sdk/client-s3') + + // Save the node to S3-compatible storage + await this.s3Client.send( + new PutObjectCommand({ + Bucket: this.bucketName, + Key: `${nodePrefix}${node.id}.json`, + Body: JSON.stringify(serializableNode, null, 2), + ContentType: 'application/json' + }) + ) + } catch (error) { + console.error(`Failed to save node ${node.id}:`, error) + throw new Error(`Failed to save node ${node.id}: ${error}`) + } + } + + /** + * Get a node from storage + */ + public async getNode(id: string): Promise { + await this.ensureInitialized() + + try { + // Get the appropriate prefix based on the node's metadata + const nodePrefix = await this.getNodePrefix(id) + + // Import the GetObjectCommand only when needed + const { GetObjectCommand } = await import('@aws-sdk/client-s3') + + try { + // Try to get the node from S3-compatible storage + const response = await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${nodePrefix}${id}.json` + }) + ) + + // Convert the response body to a string + const bodyContents = await response.Body.transformToString() + const parsedNode = JSON.parse(bodyContents) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(parsedNode.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedNode.id, + vector: parsedNode.vector, + connections + } + } catch (error) { + // If the node is not found in the expected prefix, try other prefixes + if (nodePrefix !== DEFAULT_PREFIX) { + // Try the default prefix + try { + const response = await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${NOUNS_PREFIX}${DEFAULT_PREFIX}${id}.json` + }) + ) + + const bodyContents = await response.Body.transformToString() + const parsedNode = JSON.parse(bodyContents) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries( + parsedNode.connections + )) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedNode.id, + vector: parsedNode.vector, + connections + } + } catch { + // If not found in default prefix, try all other prefixes + const prefixes = [ + PERSON_PREFIX, + PLACE_PREFIX, + THING_PREFIX, + EVENT_PREFIX, + CONCEPT_PREFIX, + CONTENT_PREFIX + ] + + for (const prefix of prefixes) { + if (prefix === nodePrefix) continue // Skip the already checked prefix + + try { + const response = await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${NOUNS_PREFIX}${prefix}${id}.json` + }) + ) + + const bodyContents = await response.Body.transformToString() + const parsedNode = JSON.parse(bodyContents) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries( + parsedNode.connections + )) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedNode.id, + vector: parsedNode.vector, + connections + } + } catch { + // Continue to the next prefix + } + } + } + } + + return null // Node not found in any prefix + } + } catch (error) { + console.error(`Failed to get node ${id}:`, error) + return null + } + } + + /** + * Get nodes by noun type + * @param nounType The noun type to filter by + * @returns Promise that resolves to an array of nodes of the specified noun type + */ + public async getNodesByNounType(nounType: string): Promise { + await this.ensureInitialized() + + try { + // Determine the prefix based on the noun type + let prefix: string + switch (nounType) { + case 'person': + prefix = PERSON_PREFIX + break + case 'place': + prefix = PLACE_PREFIX + break + case 'thing': + prefix = THING_PREFIX + break + case 'event': + prefix = EVENT_PREFIX + break + case 'concept': + prefix = CONCEPT_PREFIX + break + case 'content': + prefix = CONTENT_PREFIX + break + default: + prefix = DEFAULT_PREFIX + } + + // Import the ListObjectsV2Command and GetObjectCommand only when needed + const { ListObjectsV2Command, GetObjectCommand } = await import( + '@aws-sdk/client-s3' + ) + + // List all objects with the specified prefix + const listResponse = await this.s3Client.send( + new ListObjectsV2Command({ + Bucket: this.bucketName, + Prefix: `${NOUNS_PREFIX}${prefix}` + }) + ) + + const nodes: HNSWNode[] = [] + + // If there are no objects, return an empty array + if (!listResponse.Contents || listResponse.Contents.length === 0) { + return nodes + } + + // Get each node + const nodePromises = listResponse.Contents.map( + async (object: { Key: string }) => { + try { + const response = await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: object.Key + }) + ) + + const bodyContents = await response.Body.transformToString() + const parsedNode = JSON.parse(bodyContents) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries( + parsedNode.connections + )) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedNode.id, + vector: parsedNode.vector, + connections + } + } catch (error) { + console.error(`Failed to get node from ${object.Key}:`, error) + return null + } + } + ) + + const nodeResults = await Promise.all(nodePromises) + return nodeResults.filter((node): node is HNSWNode => node !== null) + } catch (error) { + console.error(`Failed to get nodes for noun type ${nounType}:`, error) + throw new Error(`Failed to get nodes for noun type ${nounType}: ${error}`) + } + } + + /** + * Get all nodes from storage + */ + public async getAllNodes(): Promise { + await this.ensureInitialized() + + try { + // Get all noun types + const nounTypes = [ + 'person', + 'place', + 'thing', + 'event', + 'concept', + 'content', + 'default' + ] + + // Run searches in parallel for all noun types + const nodePromises = nounTypes.map((nounType) => + this.getNodesByNounType(nounType) + ) + const nodeArrays = await Promise.all(nodePromises) + + // Combine all results + const allNodes: HNSWNode[] = [] + for (const nodes of nodeArrays) { + allNodes.push(...nodes) + } + + return allNodes + } catch (error) { + console.error('Failed to get all nodes:', error) + throw new Error(`Failed to get all nodes: ${error}`) + } + } + + /** + * Delete a node from storage + */ + public async deleteNode(id: string): Promise { + await this.ensureInitialized() + + try { + // Get the appropriate prefix based on the node's metadata + const nodePrefix = await this.getNodePrefix(id) + + // Import the DeleteObjectCommand only when needed + const { DeleteObjectCommand, GetObjectCommand } = await import( + '@aws-sdk/client-s3' + ) + + try { + // Check if the node exists before deleting + await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${nodePrefix}${id}.json` + }) + ) + + // Delete the node + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.bucketName, + Key: `${nodePrefix}${id}.json` + }) + ) + return // Node found and deleted + } catch { + // If the node is not found in the expected prefix, try other prefixes + if (nodePrefix !== DEFAULT_PREFIX) { + try { + // Try the default prefix + await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${NOUNS_PREFIX}${DEFAULT_PREFIX}${id}.json` + }) + ) + + // Delete the node + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.bucketName, + Key: `${NOUNS_PREFIX}${DEFAULT_PREFIX}${id}.json` + }) + ) + return // Node found and deleted + } catch { + // If not found in default prefix, try all other prefixes + const prefixes = [ + PERSON_PREFIX, + PLACE_PREFIX, + THING_PREFIX, + EVENT_PREFIX, + CONCEPT_PREFIX, + CONTENT_PREFIX + ] + + for (const prefix of prefixes) { + if (prefix === nodePrefix) continue // Skip the already checked prefix + + try { + await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${NOUNS_PREFIX}${prefix}${id}.json` + }) + ) + + // Delete the node + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.bucketName, + Key: `${NOUNS_PREFIX}${prefix}${id}.json` + }) + ) + return // Node found and deleted + } catch { + // Continue to the next prefix + } + } + } + } + + return // Node not found in any prefix, nothing to delete + } + } catch (error) { + console.error(`Failed to delete node ${id}:`, error) + throw new Error(`Failed to delete node ${id}: ${error}`) + } + } + + /** + * Save an edge to storage + */ + public async saveEdge(edge: Edge): Promise { + await this.ensureInitialized() + + try { + // Convert connections Map to a serializable format + const serializableEdge = { + ...edge, + connections: this.mapToObject(edge.connections, (set) => + Array.from(set as Set) + ) + } + + // Import the PutObjectCommand only when needed + const { PutObjectCommand } = await import('@aws-sdk/client-s3') + + // Save the edge to S3-compatible storage + await this.s3Client.send( + new PutObjectCommand({ + Bucket: this.bucketName, + Key: `${VERBS_PREFIX}${edge.id}.json`, + Body: JSON.stringify(serializableEdge, null, 2), + ContentType: 'application/json' + }) + ) + } catch (error) { + console.error(`Failed to save edge ${edge.id}:`, error) + throw new Error(`Failed to save edge ${edge.id}: ${error}`) + } + } + + /** + * Get an edge from storage + */ + public async getEdge(id: string): Promise { + await this.ensureInitialized() + + try { + // Import the GetObjectCommand only when needed + const { GetObjectCommand } = await import('@aws-sdk/client-s3') + + try { + // Try to get the edge from S3-compatible storage + const response = await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${VERBS_PREFIX}${id}.json` + }) + ) + + // Convert the response body to a string + const bodyContents = await response.Body.transformToString() + const parsedEdge = JSON.parse(bodyContents) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries(parsedEdge.connections)) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedEdge.id, + vector: parsedEdge.vector, + connections, + sourceId: parsedEdge.sourceId, + targetId: parsedEdge.targetId, + type: parsedEdge.type, + weight: parsedEdge.weight, + metadata: parsedEdge.metadata + } + } catch { + return null // Edge not found + } + } catch (error) { + console.error(`Failed to get edge ${id}:`, error) + return null + } + } + + /** + * Get all edges from storage + */ + public async getAllEdges(): Promise { + await this.ensureInitialized() + + try { + // Import the ListObjectsV2Command and GetObjectCommand only when needed + const { ListObjectsV2Command, GetObjectCommand } = await import( + '@aws-sdk/client-s3' + ) + + // List all objects with the edges prefix + const listResponse = await this.s3Client.send( + new ListObjectsV2Command({ + Bucket: this.bucketName, + Prefix: VERBS_PREFIX + }) + ) + + const edges: Edge[] = [] + + // If there are no objects, return an empty array + if (!listResponse.Contents || listResponse.Contents.length === 0) { + return edges + } + + // Get each edge + const edgePromises = listResponse.Contents.map( + async (object: { Key: string }) => { + try { + const response = await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: object.Key + }) + ) + + const bodyContents = await response.Body.transformToString() + const parsedEdge = JSON.parse(bodyContents) + + // Convert serialized connections back to Map> + const connections = new Map>() + for (const [level, nodeIds] of Object.entries( + parsedEdge.connections + )) { + connections.set(Number(level), new Set(nodeIds as string[])) + } + + return { + id: parsedEdge.id, + vector: parsedEdge.vector, + connections, + sourceId: parsedEdge.sourceId, + targetId: parsedEdge.targetId, + type: parsedEdge.type, + weight: parsedEdge.weight, + metadata: parsedEdge.metadata + } + } catch (error) { + console.error(`Failed to get edge from ${object.Key}:`, error) + return null + } + } + ) + + const edgeResults = await Promise.all(edgePromises) + return edgeResults.filter((edge): edge is Edge => edge !== null) + } catch (error) { + console.error('Failed to get all edges:', error) + throw new Error(`Failed to get all edges: ${error}`) + } + } + + /** + * Get edges by source node ID + */ + public async getEdgesBySource(sourceId: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllEdges() + return allEdges.filter((edge) => edge.sourceId === sourceId) + } catch (error) { + console.error(`Failed to get edges by source ${sourceId}:`, error) + throw new Error(`Failed to get edges by source ${sourceId}: ${error}`) + } + } + + /** + * Get edges by target node ID + */ + public async getEdgesByTarget(targetId: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllEdges() + return allEdges.filter((edge) => edge.targetId === targetId) + } catch (error) { + console.error(`Failed to get edges by target ${targetId}:`, error) + throw new Error(`Failed to get edges by target ${targetId}: ${error}`) + } + } + + /** + * Get edges by type + */ + public async getEdgesByType(type: string): Promise { + await this.ensureInitialized() + + try { + const allEdges = await this.getAllEdges() + return allEdges.filter((edge) => edge.type === type) + } catch (error) { + console.error(`Failed to get edges by type ${type}:`, error) + throw new Error(`Failed to get edges by type ${type}: ${error}`) + } + } + + /** + * Delete an edge from storage + */ + public async deleteEdge(id: string): Promise { + await this.ensureInitialized() + + try { + // Import the DeleteObjectCommand and GetObjectCommand only when needed + const { DeleteObjectCommand, GetObjectCommand } = await import( + '@aws-sdk/client-s3' + ) + + try { + // Check if the edge exists before deleting + await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${EDGES_PREFIX}${id}.json` + }) + ) + + // Delete the edge + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.bucketName, + Key: `${EDGES_PREFIX}${id}.json` + }) + ) + } catch { + // Edge not found, nothing to delete + return + } + } catch (error) { + console.error(`Failed to delete edge ${id}:`, error) + throw new Error(`Failed to delete edge ${id}: ${error}`) + } + } + + /** + * Save metadata to storage + */ + public async saveMetadata(id: string, metadata: any): Promise { + await this.ensureInitialized() + + try { + // Import the PutObjectCommand only when needed + const { PutObjectCommand } = await import('@aws-sdk/client-s3') + + // Save the metadata to S3-compatible storage + await this.s3Client.send( + new PutObjectCommand({ + Bucket: this.bucketName, + Key: `${METADATA_PREFIX}${id}.json`, + Body: JSON.stringify(metadata, null, 2), + ContentType: 'application/json' + }) + ) + } catch (error) { + console.error(`Failed to save metadata for ${id}:`, error) + throw new Error(`Failed to save metadata for ${id}: ${error}`) + } + } + + /** + * Get metadata from storage + */ + public async getMetadata(id: string): Promise { + await this.ensureInitialized() + + try { + // Import the GetObjectCommand only when needed + const { GetObjectCommand } = await import('@aws-sdk/client-s3') + + try { + // Try to get the metadata from S3-compatible storage + const response = await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${METADATA_PREFIX}${id}.json` + }) + ) + + // Convert the response body to a string + const bodyContents = await response.Body.transformToString() + return JSON.parse(bodyContents) + } catch { + return null // Metadata not found + } + } catch (error) { + console.error(`Failed to get metadata for ${id}:`, error) + return null + } + } + + /** + * Clear all data from storage + */ + public async clear(): Promise { + await this.ensureInitialized() + + try { + // Import the ListObjectsV2Command and DeleteObjectCommand only when needed + const { ListObjectsV2Command, DeleteObjectCommand } = await import( + '@aws-sdk/client-s3' + ) + + // List all objects in the bucket + const listResponse = await this.s3Client.send( + new ListObjectsV2Command({ + Bucket: this.bucketName + }) + ) + + // If there are no objects, return + if (!listResponse.Contents || listResponse.Contents.length === 0) { + return + } + + // Delete each object + const deletePromises = listResponse.Contents.map( + async (object: { Key: string }) => { + try { + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.bucketName, + Key: object.Key + }) + ) + } catch (error) { + console.error(`Failed to delete object ${object.Key}:`, error) + } + } + ) + + await Promise.all(deletePromises) + } catch (error) { + console.error('Failed to clear storage:', error) + throw new Error(`Failed to clear storage: ${error}`) + } + } + + /** + * Get information about storage usage and capacity + */ + public async getStorageStatus(): Promise<{ + type: string + used: number + quota: number | null + details?: Record + }> { + await this.ensureInitialized() + + try { + // Import the ListObjectsV2Command only when needed + const { ListObjectsV2Command } = await import('@aws-sdk/client-s3') + + // List all objects in the bucket to calculate total size + const listResponse = await this.s3Client.send( + new ListObjectsV2Command({ + Bucket: this.bucketName + }) + ) + + let totalSize = 0 + let nodeCount = 0 + let edgeCount = 0 + let metadataCount = 0 + + // Calculate total size and counts + if (listResponse.Contents) { + for (const object of listResponse.Contents) { + totalSize += object.Size || 0 + + const key = object.Key || '' + if (key.startsWith(NOUNS_PREFIX)) { + nodeCount++ + } else if (key.startsWith(VERBS_PREFIX)) { + edgeCount++ + } else if (key.startsWith(METADATA_PREFIX)) { + metadataCount++ + } + } + } + + // Count nodes by noun type + const nounTypeCounts: Record = { + person: 0, + place: 0, + thing: 0, + event: 0, + concept: 0, + content: 0, + default: 0 + } + + // List objects for each noun type prefix + const nounTypes = [ + { type: 'person', prefix: PERSON_PREFIX }, + { type: 'place', prefix: PLACE_PREFIX }, + { type: 'thing', prefix: THING_PREFIX }, + { type: 'event', prefix: EVENT_PREFIX }, + { type: 'concept', prefix: CONCEPT_PREFIX }, + { type: 'content', prefix: CONTENT_PREFIX }, + { type: 'default', prefix: DEFAULT_PREFIX } + ] + + for (const { type, prefix } of nounTypes) { + const listResponse = await this.s3Client.send( + new ListObjectsV2Command({ + Bucket: this.bucketName, + Prefix: `${NOUNS_PREFIX}${prefix}` + }) + ) + + nounTypeCounts[type] = listResponse.Contents?.length || 0 + } + + return { + type: this.serviceType, + used: totalSize, + quota: null, // S3-compatible services typically don't provide quota information through the API + details: { + nodeCount, + edgeCount, + metadataCount, + nounTypes: { + person: { count: nounTypeCounts.person }, + place: { count: nounTypeCounts.place }, + thing: { count: nounTypeCounts.thing }, + event: { count: nounTypeCounts.event }, + concept: { count: nounTypeCounts.concept }, + content: { count: nounTypeCounts.content }, + default: { count: nounTypeCounts.default } + } + } + } + } catch (error) { + console.error('Failed to get storage status:', error) + return { + type: this.serviceType, + used: 0, + quota: null, + details: { error: String(error) } + } + } + } + + /** + * Ensure the storage adapter is initialized + */ + private async ensureInitialized(): Promise { + if (!this.isInitialized) { + await this.init() + } + } + + /** + * Get the appropriate prefix for a node based on its metadata + */ + private async getNodePrefix(id: string): Promise { + try { + // Try to get the metadata for the node + const metadata = await this.getMetadata(id) + + // If metadata exists and has a noun field, use the corresponding prefix + if (metadata && metadata.noun) { + switch (metadata.noun) { + case 'person': + return PERSON_PREFIX + case 'place': + return PLACE_PREFIX + case 'thing': + return THING_PREFIX + case 'event': + return EVENT_PREFIX + case 'concept': + return CONCEPT_PREFIX + case 'content': + return CONTENT_PREFIX + default: + return DEFAULT_PREFIX + } + } + + // If no metadata or no noun field, use the default prefix + return DEFAULT_PREFIX + } catch (error) { + // If there's an error getting the metadata, use the default prefix + return DEFAULT_PREFIX + } + } + + /** + * Convert a Map to a plain object for serialization + */ + private mapToObject( + map: Map, + valueTransformer: (value: V) => any = (v) => v + ): Record { + const obj: Record = {} + for (const [key, value] of map.entries()) { + obj[key.toString()] = valueTransformer(value) + } + return obj + } +} diff --git a/src/types/augmentations.ts b/src/types/augmentations.ts new file mode 100644 index 00000000..660dadb2 --- /dev/null +++ b/src/types/augmentations.ts @@ -0,0 +1,413 @@ +/** Common types for the augmentation system */ + +/** + * Enum representing all types of augmentations available in the Brainy system. + */ +export enum AugmentationType { + SENSE = 'sense', + CONDUIT = 'conduit', + COGNITION = 'cognition', + MEMORY = 'memory', + PERCEPTION = 'perception', + DIALOG = 'dialog', + ACTIVATION = 'activation', + WEBSOCKET = 'webSocket' +} + +export type WebSocketConnection = { + connectionId: string + url: string + status: 'connected' | 'disconnected' | 'error' + send?: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => Promise + close?: () => Promise + _streamMessageHandler?: (event: { data: unknown }) => void + _messageHandlerWrapper?: (data: unknown) => void +} + +type DataCallback = (data: T) => void +export type AugmentationResponse = { + success: boolean + data: T + error?: string +} + +/** + * Base interface for all Brainy augmentations. + * All augmentations must implement these core properties. + */ +export interface IAugmentation { + /** A unique identifier for the augmentation (e.g., "my-reasoner-v1") */ + readonly name: string + /** A human-readable description of the augmentation's purpose */ + readonly description: string + /** Whether this augmentation is enabled */ + enabled: boolean + + /** + * Initializes the augmentation. This method is called when Brainy starts up. + * @returns A Promise that resolves when initialization is complete + */ + initialize(): Promise + + shutDown(): Promise + + getStatus(): Promise<'active' | 'inactive' | 'error'> + + // Allow string indexing for dynamic method access + [key: string]: any; +} + +/** + * Interface for WebSocket support. + * Augmentations that implement this interface can communicate via WebSockets. + */ +export interface IWebSocketSupport extends IAugmentation { + /** + * Establishes a WebSocket connection. + * @param url The WebSocket server URL to connect to + * @param protocols Optional subprotocols + * @returns A Promise resolving to a connection handle or status + */ + connectWebSocket(url: string, protocols?: string | string[]): Promise + + /** + * Sends data through an established WebSocket connection. + * @param connectionId The identifier of the established connection + * @param data The data to send (will be serialized if not a string) + */ + sendWebSocketMessage(connectionId: string, data: unknown): Promise + + /** + * Registers a callback for incoming WebSocket messages. + * @param connectionId The identifier of the established connection + * @param callback The function to call when a message is received + */ + onWebSocketMessage(connectionId: string, callback: DataCallback): Promise + + /** + * Removes a callback for incoming WebSocket messages. + * @param connectionId The identifier of the established connection + * @param callback The function to remove from the callbacks + */ + offWebSocketMessage(connectionId: string, callback: DataCallback): Promise + + /** + * Closes an established WebSocket connection. + * @param connectionId The identifier of the established connection + * @param code Optional close code + * @param reason Optional close reason + */ + closeWebSocket(connectionId: string, code?: number, reason?: string): Promise +} + +export namespace BrainyAugmentations { + /** + * Interface for Senses augmentations. + * These augmentations ingest and process raw, unstructured data into nouns and verbs. + */ + export interface ISenseAugmentation extends IAugmentation { + /** + * Processes raw input data into structured nouns and verbs. + * @param rawData The raw, unstructured data (e.g., text, image buffer, audio stream) + * @param dataType The type of raw data (e.g., 'text', 'image', 'audio') + */ + processRawData(rawData: Buffer | string, dataType: string): Promise> + + /** + * Registers a listener for real-time data feeds. + * @param feedUrl The URL or identifier of the real-time feed + * @param callback A function to call with processed data + */ + listenToFeed( + feedUrl: string, + callback: DataCallback<{ nouns: string[]; verbs: string[] }> + ): Promise + } + + /** + * Interface for Conduits augmentations. + * These augmentations establish and manage high-bandwidth, dedicated channels for structured, programmatic two-way data exchange. + */ + export interface IConduitAugmentation extends IAugmentation { + /** + * Establishes a connection for programmatic data exchange. + * @param targetSystemId The identifier of the external system to connect to + * @param config Configuration details for the connection (e.g., API keys, endpoints) + */ + establishConnection( + targetSystemId: string, + config: Record + ): Promise> + + /** + * Reads structured data directly from Brainy's knowledge graph. + * @param query A structured query (e.g., graph query language, object path) + * @param options Optional query options (e.g., depth, filters) + */ + readData( + query: Record, + options?: Record + ): Promise> + + /** + * Writes or updates structured data directly into Brainy's knowledge graph. + * @param data The structured data to write/update + * @param options Optional write options (e.g., merge, overwrite) + */ + writeData( + data: Record, + options?: Record + ): Promise> + + /** + * Monitors a specific data stream or event within Brainy for external systems. + * @param streamId The identifier of the data stream or event + * @param callback A function to call when new data/events occur + */ + monitorStream(streamId: string, callback: DataCallback): Promise + } + + /** + * Interface for Cognitions augmentations. + * These augmentations enable advanced reasoning, inference, and logical operations. + */ + export interface ICognitionAugmentation extends IAugmentation { + /** + * Performs a reasoning operation based on current knowledge. + * @param query The specific reasoning task or question + * @param context Optional additional context for the reasoning + */ + reason(query: string, context?: Record): AugmentationResponse<{ + inference: string + confidence: number + }> + + /** + * Infers relationships or new facts from existing data. + * @param dataSubset A subset of data to infer from + */ + infer(dataSubset: Record): AugmentationResponse> + + /** + * Executes a logical operation or rule set. + * @param ruleId The identifier of the rule or logic to apply + * @param input Data to apply the logic to + */ + executeLogic(ruleId: string, input: Record): AugmentationResponse + } + + /** + * Interface for Memory augmentations. + * These augmentations provide storage capabilities for data in different formats (e.g., fileSystem, in-memory). + */ + export interface IMemoryAugmentation extends IAugmentation { + /** + * Stores data in the memory system. + * @param key The unique identifier for the data + * @param data The data to store + * @param options Optional storage options (e.g., expiration, format) + */ + storeData( + key: string, + data: unknown, + options?: Record + ): Promise> + + /** + * Retrieves data from the memory system. + * @param key The unique identifier for the data + * @param options Optional retrieval options (e.g., format, version) + */ + retrieveData( + key: string, + options?: Record + ): Promise> + + /** + * Updates existing data in the memory system. + * @param key The unique identifier for the data + * @param data The updated data + * @param options Optional update options (e.g., merge, overwrite) + */ + updateData( + key: string, + data: unknown, + options?: Record + ): Promise> + + /** + * Deletes data from the memory system. + * @param key The unique identifier for the data + * @param options Optional deletion options + */ + deleteData( + key: string, + options?: Record + ): Promise> + + /** + * Lists available data keys in the memory system. + * @param pattern Optional pattern to filter keys (e.g., prefix, regex) + * @param options Optional listing options (e.g., limit, offset) + */ + listDataKeys( + pattern?: string, + options?: Record + ): Promise> + + /** + * Searches for data in the memory system using vector similarity. + * @param query The query vector or data to search for + * @param k Number of results to return + * @param options Optional search options + */ + search( + query: unknown, + k?: number, + options?: Record + ): Promise>> + } + + /** + * Interface for Perceptions augmentations. + * These augmentations interpret, contextualize, and visualize identified nouns and verbs. + */ + export interface IPerceptionAugmentation extends IAugmentation { + /** + * Interprets and contextualizes processed nouns and verbs. + * @param nouns The list of identified nouns + * @param verbs The list of identified verbs + * @param context Optional additional context for interpretation + */ + interpret( + nouns: string[], + verbs: string[], + context?: Record + ): AugmentationResponse> + + /** + * Organizes and filters information. + * @param data The data to organize (e.g., interpreted perceptions) + * @param criteria Optional criteria for filtering/prioritization + */ + organize( + data: Record, + criteria?: Record + ): AugmentationResponse> + + /** + * Generates a visualization based on the provided data. + * @param data The data to visualize (e.g., interpreted patterns) + * @param visualizationType The desired type of visualization (e.g., 'graph', 'chart') + */ + generateVisualization( + data: Record, + visualizationType: string + ): AugmentationResponse> + } + + /** + * Interface for Dialogs augmentations. + * These augmentations facilitate natural language understanding and generation for conversational interaction. + */ + export interface IDialogAugmentation extends IAugmentation { + /** + * Processes a user's natural language input (query). + * @param naturalLanguageQuery The raw text query from the user + * @param sessionId An optional session ID for conversational context + */ + processUserInput(naturalLanguageQuery: string, sessionId?: string): AugmentationResponse<{ + intent: string + nouns: string[] + verbs: string[] + context: Record + }> + + /** + * Generates a natural language response based on Brainy's knowledge and interpreted input. + * @param interpretedInput The output from `processUserInput` or similar + * @param knowledgeContext Relevant knowledge retrieved from Brainy + * @param sessionId An optional session ID for conversational context + */ + generateResponse( + interpretedInput: Record, + knowledgeContext: Record, + sessionId?: string + ): AugmentationResponse + + /** + * Manages and updates conversational context. + * @param sessionId The session ID + * @param contextUpdate The data to update the context with + */ + manageContext(sessionId: string, contextUpdate: Record): Promise + } + + /** + * Interface for Activations augmentations. + * These augmentations dictate how Brainy initiates actions, responses, or data manipulations. + */ + export interface IActivationAugmentation extends IAugmentation { + /** + * Triggers an action based on a processed command or internal state. + * @param actionName The name of the action to trigger + * @param parameters Optional parameters for the action + */ + triggerAction( + actionName: string, + parameters?: Record + ): AugmentationResponse + + /** + * Generates an expressive output or response from Brainy. + * @param knowledgeId The identifier of the knowledge to express + * @param format The desired output format (e.g., 'text', 'json') + */ + generateOutput(knowledgeId: string, format: string): AugmentationResponse> + + /** + * Interacts with an external system or API. + * @param systemId The identifier of the external system + * @param payload The data to send to the external system + */ + interactExternal(systemId: string, payload: Record): AugmentationResponse + } +} + +/** Direct exports of augmentation interfaces for easier imports */ +export interface ISenseAugmentation extends BrainyAugmentations.ISenseAugmentation { +} + +export interface IConduitAugmentation extends BrainyAugmentations.IConduitAugmentation { +} + +export interface ICognitionAugmentation extends BrainyAugmentations.ICognitionAugmentation { +} + +export interface IMemoryAugmentation extends BrainyAugmentations.IMemoryAugmentation { +} + +export interface IPerceptionAugmentation extends BrainyAugmentations.IPerceptionAugmentation { +} + +export interface IDialogAugmentation extends BrainyAugmentations.IDialogAugmentation { +} + +export interface IActivationAugmentation extends BrainyAugmentations.IActivationAugmentation { +} + +/** WebSocket-enabled augmentation interfaces */ +export type IWebSocketSenseAugmentation = BrainyAugmentations.ISenseAugmentation & IWebSocketSupport +export type IWebSocketConduitAugmentation = BrainyAugmentations.IConduitAugmentation & IWebSocketSupport +export type IWebSocketCognitionAugmentation = BrainyAugmentations.ICognitionAugmentation & IWebSocketSupport +export type IWebSocketMemoryAugmentation = BrainyAugmentations.IMemoryAugmentation & IWebSocketSupport +export type IWebSocketPerceptionAugmentation = BrainyAugmentations.IPerceptionAugmentation & IWebSocketSupport +export type IWebSocketDialogAugmentation = BrainyAugmentations.IDialogAugmentation & IWebSocketSupport +export type IWebSocketActivationAugmentation = BrainyAugmentations.IActivationAugmentation & IWebSocketSupport diff --git a/src/types/brainyDataInterface.ts b/src/types/brainyDataInterface.ts new file mode 100644 index 00000000..414932d5 --- /dev/null +++ b/src/types/brainyDataInterface.ts @@ -0,0 +1,55 @@ +/** + * BrainyDataInterface + * + * This interface defines the methods from BrainyData that are used by serverSearchAugmentations.ts. + * It's used to break the circular dependency between brainyData.ts and serverSearchAugmentations.ts. + */ + +import { Vector } from '../coreTypes.js' + +export interface BrainyDataInterface { + /** + * Initialize the database + */ + init(): Promise + + /** + * Get a noun by ID + * @param id The ID of the noun to get + */ + get(id: string): Promise + + /** + * Add a vector or data to the database + * @param vectorOrData Vector or data to add + * @param metadata Optional metadata to associate with the vector + * @returns The ID of the added vector + */ + add(vectorOrData: Vector | unknown, metadata?: T): Promise + + /** + * Search for text in the database + * @param text The text to search for + * @param limit Maximum number of results to return + * @returns Search results + */ + searchText(text: string, limit?: number): Promise + + /** + * Create a relationship between two entities + * @param sourceId The ID of the source entity + * @param targetId The ID of the target entity + * @param relationType The type of relationship + * @param metadata Optional metadata about the relationship + * @returns The ID of the created relationship + */ + relate(sourceId: string, targetId: string, relationType: string, metadata?: unknown): Promise + + /** + * Find entities similar to a given entity ID + * @param id ID of the entity to find similar entities for + * @param options Additional options + * @returns Array of search results with similarity scores + */ + findSimilar(id: string, options?: { limit?: number }): Promise +} diff --git a/src/types/fileSystemTypes.ts b/src/types/fileSystemTypes.ts new file mode 100644 index 00000000..3b488d83 --- /dev/null +++ b/src/types/fileSystemTypes.ts @@ -0,0 +1,14 @@ +/** + * Type declarations for the File System Access API + * Extends the FileSystemDirectoryHandle interface to include the [Symbol.asyncIterator] method + */ + +// Extend the FileSystemDirectoryHandle interface +interface FileSystemDirectoryHandle { + [Symbol.asyncIterator](): AsyncIterableIterator<[string, FileSystemHandle]>; + keys(): AsyncIterableIterator; + entries(): AsyncIterableIterator<[string, FileSystemHandle]>; +} + +// Export something to make this a module +export const fileSystemTypesLoaded = true; diff --git a/src/types/graphTypes.ts b/src/types/graphTypes.ts new file mode 100644 index 00000000..7e1d7b79 --- /dev/null +++ b/src/types/graphTypes.ts @@ -0,0 +1,150 @@ +// Common metadata types +/** + * Represents a high-precision timestamp with seconds and nanoseconds + * Used for tracking creation and update times of graph elements + */ +interface Timestamp { + seconds: number + nanoseconds: number +} + +/** + * Metadata about the creator/source of a graph noun + * Tracks which augmentation and model created the element + */ +interface CreatorMetadata { + augmentation: string // Name of the augmentation that created this element + version: string // Version of the augmentation +} + +/** + * Base interface for nodes (nouns) in the graph + * Represents entities like people, places, things, etc. + */ +export interface GraphNoun { + id: string // Unique identifier for the noun + createdBy: CreatorMetadata // Information about what created this noun + noun: NounType // Type classification of the noun + createdAt: Timestamp // When the noun was created + updatedAt: Timestamp // When the noun was last updated + label?: string // Optional descriptive label + data?: Record // Additional flexible data storage + embeddedVerbs?: EmbeddedGraphVerb[] // Optional embedded relationships + embedding?: number[] // Vector representation of the noun +} + +/** + * Base interface for edges (verbs) in the graph + * Represents relationships between nouns + */ +export interface GraphVerb { + id: string // Unique identifier for the verb + source: string // ID of the source noun + target: string // ID of the target noun + label?: string // Optional descriptive label + verb: VerbType // Type of relationship + createdAt: Timestamp // When the verb was created + updatedAt: Timestamp // When the verb was last updated + data?: Record // Additional flexible data storage + embedding?: number[] // Vector representation of the relationship + confidence?: number // Confidence score (0-1) + weight?: number // Strength/importance of the relationship +} + +/** + * Version of GraphVerb for embedded relationships + * Used when the source is implicit from the parent document + */ +export type EmbeddedGraphVerb = Omit + +// Proper Noun interfaces - extend GraphNoun with specific noun types + +/** + * Represents a person entity in the graph + */ +export interface Person extends GraphNoun { + noun: typeof NounType.Person +} + +/** + * Represents a physical location in the graph + */ +export interface Place extends GraphNoun { + noun: typeof NounType.Place +} + +/** + * Represents a physical or virtual object in the graph + */ +export interface Thing extends GraphNoun { + noun: typeof NounType.Thing +} + +/** + * Represents an event or occurrence in the graph + */ +export interface Event extends GraphNoun { + noun: typeof NounType.Event +} + +/** + * Represents an abstract concept or idea in the graph + */ +export interface Concept extends GraphNoun { + noun: typeof NounType.Concept +} + +export interface Group extends GraphNoun { + noun: typeof NounType.Group +} + +export interface List extends GraphNoun { + noun: typeof NounType.List +} + +/** + * Represents content (text, media, etc.) in the graph + */ +export interface Content extends GraphNoun { + noun: typeof NounType.Content +} + +/** + * Defines valid noun types for graph entities + * Used for categorizing different types of nodes + */ + +export const NounType = { + Person: 'person', // Person entities + Place: 'place', // Physical locations + Thing: 'thing', // Physical or virtual objects + Event: 'event', // Events or occurrences + Concept: 'concept', // Abstract concepts or ideas + Content: 'content', // Content items + Group: 'group', // Groups of related entities + List: 'list', // Ordered collections of entities + Category: 'category' // Categories for content items including tags +} as const +export type NounType = (typeof NounType)[keyof typeof NounType] + +/** + * Defines valid verb types for relationships + * Used for categorizing different types of connections + */ +export const VerbType = { + AttributedTo: 'attributedTo', // Indicates attribution or authorship + Controls: 'controls', // Indicates control or ownership + Created: 'created', // Indicates creation or authorship + Earned: 'earned', // Indicates achievement or acquisition + Owns: 'owns', // Indicates ownership + MemberOf: 'memberOf', // Indicates membership or affiliation + RelatedTo: 'relatedTo', // Indicates family relationship + WorksWith: 'worksWith', // Indicates professional relationship + FriendOf: 'friendOf', // Indicates friendship + ReportsTo: 'reportsTo', // Indicates reporting relationship + Supervises: 'supervises', // Indicates supervisory relationship + Mentors: 'mentors', // Indicates mentorship relationship + Follows: 'follows', // Indicates the following relationship + Likes: 'likes' // Indicates liking relationship +} as const +export type VerbType = (typeof VerbType)[keyof typeof VerbType] diff --git a/src/types/mcpTypes.ts b/src/types/mcpTypes.ts new file mode 100644 index 00000000..965dcc50 --- /dev/null +++ b/src/types/mcpTypes.ts @@ -0,0 +1,149 @@ +/** + * Model Control Protocol (MCP) Types + * + * This file defines the types and interfaces for the Model Control Protocol (MCP) + * implementation in Brainy. MCP allows external models to access Brainy data and + * use the augmentation pipeline as tools. + */ + +/** + * MCP version information + */ +export const MCP_VERSION = '1.0.0' + +/** + * MCP request types + */ +export enum MCPRequestType { + DATA_ACCESS = 'data_access', + TOOL_EXECUTION = 'tool_execution', + SYSTEM_INFO = 'system_info', + AUTHENTICATION = 'authentication' +} + +/** + * Base interface for all MCP requests + */ +export interface MCPRequest { + /** The type of request */ + type: MCPRequestType + /** Request ID for tracking and correlation */ + requestId: string + /** API version */ + version: string + /** Authentication token (if required) */ + authToken?: string +} + +/** + * Interface for data access requests + */ +export interface MCPDataAccessRequest extends MCPRequest { + type: MCPRequestType.DATA_ACCESS + /** The data access operation to perform */ + operation: 'get' | 'search' | 'add' | 'getRelationships' + /** Parameters for the operation */ + parameters: Record +} + +/** + * Interface for tool execution requests + */ +export interface MCPToolExecutionRequest extends MCPRequest { + type: MCPRequestType.TOOL_EXECUTION + /** The name of the tool to execute */ + toolName: string + /** Parameters for the tool */ + parameters: Record +} + +/** + * Interface for system info requests + */ +export interface MCPSystemInfoRequest extends MCPRequest { + type: MCPRequestType.SYSTEM_INFO + /** The type of information to retrieve */ + infoType: 'status' | 'availableTools' | 'version' +} + +/** + * Interface for authentication requests + */ +export interface MCPAuthenticationRequest extends MCPRequest { + type: MCPRequestType.AUTHENTICATION + /** The authentication credentials */ + credentials: { + apiKey?: string + username?: string + password?: string + } +} + +/** + * Base interface for all MCP responses + */ +export interface MCPResponse { + /** Whether the request was successful */ + success: boolean + /** The request ID from the original request */ + requestId: string + /** API version */ + version: string + /** Response data (if successful) */ + data?: any + /** Error information (if unsuccessful) */ + error?: { + code: string + message: string + details?: any + } +} + +/** + * Interface for MCP tool definitions + */ +export interface MCPTool { + /** The name of the tool */ + name: string + /** A description of what the tool does */ + description: string + /** The parameters the tool accepts */ + parameters: { + type: 'object' + properties: Record + required: string[] + } +} + +/** + * Configuration options for MCP services + */ +export interface MCPServiceOptions { + /** Port for the WebSocket server */ + wsPort?: number + /** Port for the REST server */ + restPort?: number + /** Whether to enable authentication */ + enableAuth?: boolean + /** API keys for authentication */ + apiKeys?: string[] + /** Rate limiting configuration */ + rateLimit?: { + /** Maximum number of requests per window */ + maxRequests: number + /** Time window in milliseconds */ + windowMs: number + } + /** CORS configuration for REST API */ + cors?: { + /** Allowed origins */ + origin: string | string[] + /** Whether to allow credentials */ + credentials: boolean + } +} diff --git a/src/types/pipelineTypes.ts b/src/types/pipelineTypes.ts new file mode 100644 index 00000000..9c1755b2 --- /dev/null +++ b/src/types/pipelineTypes.ts @@ -0,0 +1,33 @@ +/** + * Pipeline Types + * + * This module provides shared types for the pipeline system to avoid circular dependencies. + */ + +import { + BrainyAugmentations, + IWebSocketSupport, + IAugmentation +} from './augmentations.js' + +/** + * Type definitions for the augmentation registry + */ +export type AugmentationRegistry = { + sense: BrainyAugmentations.ISenseAugmentation[]; + conduit: BrainyAugmentations.IConduitAugmentation[]; + cognition: BrainyAugmentations.ICognitionAugmentation[]; + memory: BrainyAugmentations.IMemoryAugmentation[]; + perception: BrainyAugmentations.IPerceptionAugmentation[]; + dialog: BrainyAugmentations.IDialogAugmentation[]; + activation: BrainyAugmentations.IActivationAugmentation[]; + webSocket: IWebSocketSupport[]; +} + +/** + * Interface for the Pipeline class + * This is used to break circular dependencies between pipeline.ts and augmentationRegistry.ts + */ +export interface IPipeline { + register(augmentation: T): IPipeline; +} diff --git a/src/types/tensorflowTypes.ts b/src/types/tensorflowTypes.ts new file mode 100644 index 00000000..94a43484 --- /dev/null +++ b/src/types/tensorflowTypes.ts @@ -0,0 +1,14 @@ +// Type declarations for TensorFlow.js models + +// This file is a placeholder for TensorFlow.js model types +// We're using type assertions in the code instead of module declarations + +// Define some basic types that might be useful +export interface TensorflowModel { + load(): Promise; + embed(data: string[]): any; + dispose(): void; +} + +// Export a dummy constant to make this a proper module +export const tensorflowModelsLoaded = true; diff --git a/src/unified.ts b/src/unified.ts new file mode 100644 index 00000000..a2dbd38f --- /dev/null +++ b/src/unified.ts @@ -0,0 +1,36 @@ +/** + * Unified entry point for Brainy + * This file exports everything from index.ts + * Environment detection is handled here and made available to all components + */ + +// Export environment information +export const environment = { + isBrowser: typeof window !== 'undefined', + isNode: + typeof process !== 'undefined' && process.versions && process.versions.node, + isServerless: + typeof window === 'undefined' && + (typeof process === 'undefined' || + !process.versions || + !process.versions.node) +} + +// Make environment information available globally +if (typeof globalThis !== 'undefined') { + globalThis.__ENV__ = environment +} + +// Log the detected environment +console.log( + `Brainy running in ${ + environment.isBrowser + ? 'browser' + : environment.isNode + ? 'Node.js' + : 'serverless/unknown' + } environment` +) + +// Re-export everything from index.ts +export * from './index.js' diff --git a/src/utils/distance.ts b/src/utils/distance.ts new file mode 100644 index 00000000..a7c98560 --- /dev/null +++ b/src/utils/distance.ts @@ -0,0 +1,86 @@ +/** + * Distance functions for vector similarity calculations + * Optimized for Node.js 23.11+ using enhanced array methods + */ + +import { DistanceFunction, Vector } from '../coreTypes.js' + +/** + * Calculates the Euclidean distance between two vectors + * Lower values indicate higher similarity + * Optimized using array methods for Node.js 23.11+ + */ +export const euclideanDistance: DistanceFunction = (a: Vector, b: Vector): number => { + if (a.length !== b.length) { + throw new Error('Vectors must have the same dimensions') + } + + // Use array.reduce for better performance in Node.js 23.11+ + const sum = a.reduce((acc, val, i) => { + const diff = val - b[i] + return acc + (diff * diff) + }, 0) + + return Math.sqrt(sum) +} + +/** + * Calculates the cosine distance between two vectors + * Lower values indicate higher similarity + * Range: 0 (identical) to 2 (opposite) + * Optimized using array methods for Node.js 23.11+ + */ +export const cosineDistance: DistanceFunction = (a: Vector, b: Vector): number => { + if (a.length !== b.length) { + throw new Error('Vectors must have the same dimensions') + } + + // Use array.reduce to calculate all values in a single pass + const { dotProduct, normA, normB } = a.reduce((acc, val, i) => { + return { + dotProduct: acc.dotProduct + (val * b[i]), + normA: acc.normA + (val * val), + normB: acc.normB + (b[i] * b[i]) + }; + }, { dotProduct: 0, normA: 0, normB: 0 }); + + if (normA === 0 || normB === 0) { + return 2 // Maximum distance for zero vectors + } + + const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)) + // Convert cosine similarity (-1 to 1) to distance (0 to 2) + return 1 - similarity +} + +/** + * Calculates the Manhattan (L1) distance between two vectors + * Lower values indicate higher similarity + * Optimized using array methods for Node.js 23.11+ + */ +export const manhattanDistance: DistanceFunction = (a: Vector, b: Vector): number => { + if (a.length !== b.length) { + throw new Error('Vectors must have the same dimensions') + } + + // Use array.reduce for better performance in Node.js 23.11+ + return a.reduce((sum, val, i) => sum + Math.abs(val - b[i]), 0) +} + +/** + * Calculates the dot product similarity between two vectors + * Higher values indicate higher similarity + * Converted to a distance metric (lower is better) + * Optimized using array methods for Node.js 23.11+ + */ +export const dotProductDistance: DistanceFunction = (a: Vector, b: Vector): number => { + if (a.length !== b.length) { + throw new Error('Vectors must have the same dimensions') + } + + // Use array.reduce for better performance in Node.js 23.11+ + const dotProduct = a.reduce((sum, val, i) => sum + (val * b[i]), 0) + + // Convert to a distance metric (lower is better) + return -dotProduct +} diff --git a/src/utils/embedding.ts b/src/utils/embedding.ts new file mode 100644 index 00000000..495a49fb --- /dev/null +++ b/src/utils/embedding.ts @@ -0,0 +1,293 @@ +/** + * Embedding functions for converting data to vectors + */ + +import { EmbeddingFunction, EmbeddingModel, Vector } from '../coreTypes.js' +import { executeInThread } from './workerUtils.js' + +/** + * TensorFlow Universal Sentence Encoder embedding model + * This model provides high-quality text embeddings using TensorFlow.js + * The required TensorFlow.js dependencies are automatically installed with this package + */ +export class UniversalSentenceEncoder implements EmbeddingModel { + private model: any = null + private initialized = false + private tf: any = null + private use: any = null + + /** + * Initialize the embedding model + */ + public async init(): Promise { + try { + // Save original console.warn + const originalWarn = console.warn + + // Override console.warn to suppress TensorFlow.js Node.js backend message + console.warn = function (message?: any, ...optionalParams: any[]) { + if ( + message && + typeof message === 'string' && + message.includes( + 'Hi, looks like you are running TensorFlow.js in Node.js' + ) + ) { + return // Suppress the specific warning + } + originalWarn(message, ...optionalParams) + } + + // Define EPSILON flag before TensorFlow.js is loaded + // This prevents the "Cannot evaluate flag 'EPSILON': no evaluation function found" error + if (typeof window !== 'undefined') { + ;(window as any).EPSILON = 1e-7 + // Define the flag with an evaluation function for TensorFlow.js + ;(window as any).ENV = (window as any).ENV || {} + ;(window as any).ENV.flagRegistry = + (window as any).ENV.flagRegistry || {} + ;(window as any).ENV.flagRegistry.EPSILON = { + evaluationFn: () => 1e-7 + } + } else if (typeof global !== 'undefined') { + ;(global as any).EPSILON = 1e-7 + // Define the flag with an evaluation function for TensorFlow.js + ;(global as any).ENV = (global as any).ENV || {} + ;(global as any).ENV.flagRegistry = + (global as any).ENV.flagRegistry || {} + ;(global as any).ENV.flagRegistry.EPSILON = { + evaluationFn: () => 1e-7 + } + } + + // Dynamically import TensorFlow.js and Universal Sentence Encoder + // Use type assertions to tell TypeScript these modules exist + this.tf = await import('@tensorflow/tfjs') + this.use = await import('@tensorflow-models/universal-sentence-encoder') + + // Load the model + this.model = await this.use.load() + this.initialized = true + + // Restore original console.warn + console.warn = originalWarn + } catch (error) { + console.error('Failed to initialize Universal Sentence Encoder:', error) + throw new Error( + `Failed to initialize Universal Sentence Encoder: ${error}` + ) + } + } + + /** + * Embed text into a vector using Universal Sentence Encoder + * @param data Text to embed + */ + public async embed(data: string | string[]): Promise { + if (!this.initialized) { + await this.init() + } + + try { + // Handle different input types + let textToEmbed: string[] + if (typeof data === 'string') { + // Handle empty string case + if (data.trim() === '') { + // Return a zero vector of appropriate dimension (512 is the default for USE) + return new Array(512).fill(0) + } + textToEmbed = [data] + } else if ( + Array.isArray(data) && + data.every((item) => typeof item === 'string') + ) { + // Handle empty array or array with empty strings + if (data.length === 0 || data.every((item) => item.trim() === '')) { + return new Array(512).fill(0) + } + // Filter out empty strings + textToEmbed = data.filter((item) => item.trim() !== '') + if (textToEmbed.length === 0) { + return new Array(512).fill(0) + } + } else { + throw new Error( + 'UniversalSentenceEncoder only supports string or string[] data' + ) + } + + // Get embeddings + const embeddings = await this.model.embed(textToEmbed) + + // Convert to array and return the first embedding + const embeddingArray = await embeddings.array() + return embeddingArray[0] + } catch (error) { + console.error( + 'Failed to embed text with Universal Sentence Encoder:', + error + ) + throw new Error( + `Failed to embed text with Universal Sentence Encoder: ${error}` + ) + } + } + + /** + * Dispose of the model resources + */ + public async dispose(): Promise { + if (this.model && this.tf) { + try { + // Dispose of the model and tensors + this.model.dispose() + this.tf.disposeVariables() + this.initialized = false + } catch (error) { + console.error('Failed to dispose Universal Sentence Encoder:', error) + } + } + return Promise.resolve() + } +} + +/** + * Create an embedding function from an embedding model + * @param model Embedding model to use + */ +export function createEmbeddingFunction( + model: EmbeddingModel +): EmbeddingFunction { + return async (data: any): Promise => { + return await model.embed(data) + } +} + +/** + * Creates a TensorFlow-based Universal Sentence Encoder embedding function + * This is the required embedding function for all text embeddings + */ +export function createTensorFlowEmbeddingFunction(): EmbeddingFunction { + // Create a single shared instance of the model + const model = new UniversalSentenceEncoder() + let modelInitialized = false + + return async (data: any): Promise => { + try { + // Initialize the model if it hasn't been initialized yet + if (!modelInitialized) { + await model.init() + modelInitialized = true + } + + return await model.embed(data) + } catch (error) { + console.error('Failed to use TensorFlow embedding:', error) + throw new Error( + `Universal Sentence Encoder is required but failed: ${error}` + ) + } + } +} + +/** + * Creates a TensorFlow-based Universal Sentence Encoder embedding function that runs in a separate thread + * This provides better performance for CPU-intensive embedding operations + * @param options Configuration options + * @returns An embedding function that runs in a separate thread + */ +export function createThreadedEmbeddingFunction( + options: { fallbackToMain?: boolean } = {} +): EmbeddingFunction { + // Create a standard embedding function to use as fallback + const standardEmbedding = createTensorFlowEmbeddingFunction() + + // Flag to track if we've fallen back to main thread + let useFallback = false + + return async (data: any): Promise => { + // If we've already determined that threading doesn't work, use the fallback + if (useFallback) { + return standardEmbedding(data) + } + + try { + // Function to be executed in a worker thread + // This must be a regular function (not async) to avoid Promise cloning issues + const embedInWorker = (inputData: any) => { + // Return a plain object with the input data + // All async operations will be performed inside the worker + return { data: inputData } + } + + // Worker implementation function that will be stringified and run in the worker + const workerImplementation = async ({ data }: { data: any }) => { + // We need to dynamically import TensorFlow.js and USE in the worker + const tf = await import('@tensorflow/tfjs') + const use = await import('@tensorflow-models/universal-sentence-encoder') + + // Load the model + const model = await use.load() + + // Handle different input types + let textToEmbed: string[] + if (typeof data === 'string') { + if (data.trim() === '') { + return new Array(512).fill(0) + } + textToEmbed = [data] + } else if ( + Array.isArray(data) && + data.every((item) => typeof item === 'string') + ) { + if (data.length === 0 || data.every((item) => item.trim() === '')) { + return new Array(512).fill(0) + } + textToEmbed = data.filter((item) => item.trim() !== '') + if (textToEmbed.length === 0) { + return new Array(512).fill(0) + } + } else { + throw new Error( + 'UniversalSentenceEncoder only supports string or string[] data' + ) + } + + // Get embeddings + const embeddings = await model.embed(textToEmbed) + + // Convert to array and return the first embedding + const embeddingArray = await embeddings.array() + + // Dispose of the tensor to free memory + embeddings.dispose() + + return embeddingArray[0] + } + + // Execute the embedding function in a separate thread + // Pass the worker implementation as a string to avoid Promise cloning issues + return await executeInThread(workerImplementation.toString(), embedInWorker(data)) + } catch (error) { + // If threading fails and fallback is enabled, use the standard embedding function + if (options.fallbackToMain) { + console.warn('Threaded embedding failed, falling back to main thread:', error) + useFallback = true + return standardEmbedding(data) + } + + // Otherwise, propagate the error + throw new Error(`Threaded embedding failed: ${error}`) + } + } +} + +/** + * Default embedding function + * Uses UniversalSentenceEncoder for all text embeddings + * TensorFlow.js is required for this to work + * Uses threading when available for better performance + */ +export const defaultEmbeddingFunction: EmbeddingFunction = + createThreadedEmbeddingFunction({ fallbackToMain: true }) diff --git a/src/utils/environment.ts b/src/utils/environment.ts new file mode 100644 index 00000000..477b07ca --- /dev/null +++ b/src/utils/environment.ts @@ -0,0 +1,57 @@ +/** + * Utility functions for environment detection + */ + +/** + * Check if code is running in a browser environment + */ +export function isBrowser(): boolean { + return typeof window !== 'undefined' && typeof document !== 'undefined'; +} + +/** + * Check if code is running in a Node.js environment + */ +export function isNode(): boolean { + return typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null; +} + +/** + * Check if code is running in a Web Worker environment + */ +export function isWebWorker(): boolean { + return typeof self === 'object' && + self.constructor && + self.constructor.name === 'DedicatedWorkerGlobalScope'; +} + +/** + * Check if Web Workers are available in the current environment + */ +export function areWebWorkersAvailable(): boolean { + return isBrowser() && typeof Worker !== 'undefined'; +} + +/** + * Check if Worker Threads are available in the current environment (Node.js) + */ +export function areWorkerThreadsAvailable(): boolean { + if (!isNode()) return false; + + try { + // Dynamic import to avoid errors in browser environments + require('worker_threads'); + return true; + } catch (e) { + return false; + } +} + +/** + * Determine if threading is available in the current environment + */ +export function isThreadingAvailable(): boolean { + return areWebWorkersAvailable() || areWorkerThreadsAvailable(); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..11db368e --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './distance.js' +export * from './embedding.js' diff --git a/src/utils/version.ts b/src/utils/version.ts new file mode 100644 index 00000000..2a5fc5f6 --- /dev/null +++ b/src/utils/version.ts @@ -0,0 +1,6 @@ +/** + * This file is auto-generated during the build process. + * Do not modify this file directly. + */ + +export const VERSION = '0.9.10'; diff --git a/src/utils/workerUtils.ts b/src/utils/workerUtils.ts new file mode 100644 index 00000000..263d4d30 --- /dev/null +++ b/src/utils/workerUtils.ts @@ -0,0 +1,145 @@ +/** + * Utility functions for working with Web Workers and Worker Threads + */ + +import { isNode, isBrowser } from './environment.js' + +/** + * Execute a function in a Web Worker (browser environment) + * + * @param fnString The function to execute as a string + * @param args The arguments to pass to the function + * @returns A promise that resolves with the result of the function + */ +export function executeInWebWorker(fnString: string, args: any): Promise { + return new Promise((resolve, reject) => { + // Create a blob URL for the worker script + const workerScript = ` + self.onmessage = async function(e) { + try { + const fn = ${fnString}; + const result = await fn(e.data); + self.postMessage({ success: true, data: result }); + } catch (error) { + self.postMessage({ + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } + }; + ` + + const blob = new Blob([workerScript], { type: 'application/javascript' }) + const blobURL = URL.createObjectURL(blob) + + // Create a worker + const worker = new Worker(blobURL) + + // Set up message handling + worker.onmessage = function ( + e: MessageEvent<{ success: boolean; data: T; error?: string }> + ) { + URL.revokeObjectURL(blobURL) // Clean up + worker.terminate() // Terminate the worker + + if (e.data.success) { + resolve(e.data.data) + } else { + reject(new Error(e.data.error)) + } + } + + worker.onerror = function (error: ErrorEvent) { + URL.revokeObjectURL(blobURL) // Clean up + worker.terminate() // Terminate the worker + reject(error) + } + + // Start the worker + worker.postMessage(args) + }) +} + +/** + * Execute a function in a Worker Thread (Node.js environment) + * + * @param fnString The function to execute as a string + * @param args The arguments to pass to the function + * @returns A promise that resolves with the result of the function + */ +export function executeInWorkerThread( + fnString: string, + args: any +): Promise { + return new Promise((resolve, reject) => { + try { + // Dynamic import to avoid errors in browser environments + const { Worker } = require('worker_threads') + + // Create a worker script + const workerScript = ` + const { parentPort } = require('worker_threads'); + + parentPort.once('message', async (data) => { + try { + const fn = ${fnString}; + const result = await fn(data); + parentPort.postMessage({ success: true, data: result }); + } catch (error) { + parentPort.postMessage({ + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } + }); + ` + + // Create a worker + const worker = new Worker(workerScript, { eval: true }) + + // Set up message handling + worker.on( + 'message', + (data: { success: boolean; data: T; error?: string }) => { + worker.terminate() // Terminate the worker + + if (data.success) { + resolve(data.data) + } else { + reject(new Error(data.error)) + } + } + ) + + worker.on('error', (error: Error) => { + worker.terminate() // Terminate the worker + reject(error) + }) + + // Start the worker + worker.postMessage(args) + } catch (error) { + reject(error) + } + }) +} + +/** + * Execute a function in a separate thread based on the environment + * + * @param fnString The function to execute as a string + * @param args The arguments to pass to the function + * @returns A promise that resolves with the result of the function + */ +export function executeInThread(fnString: string, args: any): Promise { + if (isBrowser()) { + return executeInWebWorker(fnString, args) + } else if (isNode()) { + return executeInWorkerThread(fnString, args) + } else { + // Fall back to executing in the main thread + // Parse the function from string and execute it + const fn = new Function('return ' + fnString)() + return Promise.resolve(fn(args) as T) + } +} diff --git a/tsconfig.browser.json b/tsconfig.browser.json new file mode 100644 index 00000000..b8deca5b --- /dev/null +++ b/tsconfig.browser.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "declaration": false, + "outDir": "./dist", + "rootDir": ".", + "lib": [ + "DOM", + "ESNext" + ], + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": false, + "preserveConstEnums": true, + "sourceMap": true + }, + "include": [ + "src/**/*", + "demo/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..a72fcfc0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "lib": [ + "DOM", + "ESNext" + ], + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": false, + "preserveConstEnums": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts" + ] +} diff --git a/tsconfig.unified.json b/tsconfig.unified.json new file mode 100644 index 00000000..a72fcfc0 --- /dev/null +++ b/tsconfig.unified.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "lib": [ + "DOM", + "ESNext" + ], + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": false, + "preserveConstEnums": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts" + ] +} diff --git a/verify-package-size.js b/verify-package-size.js new file mode 100755 index 00000000..400369ef --- /dev/null +++ b/verify-package-size.js @@ -0,0 +1,135 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import { execSync } from 'child_process' +import { fileURLToPath } from 'url' + +// Get the current directory +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// Function to get the size of a file in MB +function getFileSizeInMB(filePath) { + const stats = fs.statSync(filePath) + return stats.size / (1024 * 1024) +} + +// Function to check if a file should be included in the package +function shouldIncludeFile(filePath, npmignorePatterns, includePatterns) { + const relativePath = path.relative('.', filePath) + + // Check if the file matches any npmignore pattern + for (const pattern of npmignorePatterns) { + if (pattern.test(relativePath)) { + return false + } + } + + // If we have explicit include patterns, check if the file matches any + if (includePatterns.length > 0) { + for (const pattern of includePatterns) { + if (pattern.test(relativePath)) { + return true + } + } + return false + } + + return true +} + +// Parse .npmignore file +function parseNpmignore() { + const patterns = [] + if (fs.existsSync('.npmignore')) { + const content = fs.readFileSync('.npmignore', 'utf8') + const lines = content.split('\n') + + for (const line of lines) { + const trimmedLine = line.trim() + if (trimmedLine && !trimmedLine.startsWith('#')) { + // Convert glob pattern to regex + let pattern = trimmedLine + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace(/\?/g, '.') + + // Handle directory patterns + if (pattern.endsWith('/')) { + pattern = `${pattern}.*` + } + + patterns.push(new RegExp(`^${pattern}$`)) + } + } + } + return patterns +} + +// Parse package.json files array +function parsePackageFiles() { + const patterns = [] + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')) + + if (packageJson.files && Array.isArray(packageJson.files)) { + for (const pattern of packageJson.files) { + // Convert glob pattern to regex + let regexPattern = pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace(/\?/g, '.') + + // Handle directory patterns + if (regexPattern.endsWith('/')) { + regexPattern = `${regexPattern}.*` + } + + patterns.push(new RegExp(`^${regexPattern}$`)) + } + } + + return patterns +} + +// Calculate the total size of files that would be included in the package +function calculatePackageSize() { + const npmignorePatterns = parseNpmignore() + const includePatterns = parsePackageFiles() + + let totalSize = 0 + let includedFiles = [] + + function processDirectory(dirPath) { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name) + + if (entry.isDirectory()) { + processDirectory(fullPath) + } else if (entry.isFile()) { + if (shouldIncludeFile(fullPath, npmignorePatterns, includePatterns)) { + const sizeInMB = getFileSizeInMB(fullPath) + totalSize += sizeInMB + includedFiles.push({ path: fullPath, size: sizeInMB }) + } + } + } + } + + processDirectory('.') + + // Sort files by size (largest first) + includedFiles.sort((a, b) => b.size - a.size) + + console.log('Estimated package size: ' + totalSize.toFixed(2) + ' MB') + console.log('\nLargest files:') + for (let i = 0; i < Math.min(10, includedFiles.length); i++) { + console.log( + `${includedFiles[i].path}: ${includedFiles[i].size.toFixed(2)} MB` + ) + } +} + +calculatePackageSize()
+ + +
+ Brainy Logo +
+

Brainy Interactive Demo

+

A powerful graph & vector data platform for AI applications across any environment

+
+
+ + +
+

🚀 Key Features

+

Explore all of Brainy's powerful capabilities in this interactive demo:

+ +
+
+

🏃‍♂️ Run Everywhere

+

Run Brainy in a browser, container, serverless cloud service or the terminal!

+
+
+

🔍 Vector Search

+

Find semantically similar content using embeddings (like having ESP for your data!)

+
+
+

🌐 Graph Database

+

Connect data with meaningful relationships (your data's social network)

+
+
+

🌊 Streaming Pipeline

+

Process data in real-time as it flows through the system (like a data waterslide!)

+
+
+

🧩 Extensible Augmentations

+

Customize and extend functionality with pluggable components (LEGO blocks for your data!)

+
+
+

🔌 Built-in Conduits

+

Sync and scale across instances with WebSocket and WebRTC (your data's teleportation system!)

+
+
+

🤖 TensorFlow Integration

+

Use TensorFlow.js for high-quality embeddings (included as a required dependency)

+
+
+

🧠 Adaptive Intelligence

+

Automatically optimizes for your environment and usage patterns

+
+
+

🌍 Cross-Platform

+

Works everywhere you do: browsers, Node.js and server environments

+
+
+

💾 Persistent Storage

+

Data persists across sessions and scales to any size (no memory loss here!)

+
+
+

🔧 TypeScript Support

+

Fully typed API with generics (for those who like their code tidy)

+
+
+

📱 CLI Tools

+

Powerful command-line interface for data management (command line wizardry)

+
+
+

📊 Data Visualization

+

Interactive graph and vector space visualizations

+
+
+

☁️ Cloud Ready

+

Easy deployment to Google, AWS, Azure and other cloud platforms

+
+
+

🎮 Model Control Protocol (MCP)

+

Allow external AI models to access Brainy data and use augmentation pipeline as tools

+
+
+
+ + +
+

📊 What Can You Build?

+
+
+

🔍 Semantic Search Engines

+

Find content based on meaning, not just keywords

+
+
+

👍 Recommendation Systems

+

Suggest similar items based on vector similarity

+
+
+

🧠 Knowledge Graphs

+

Build connected data structures with relationships

+
+
+

🤖 AI Applications

+

Store and retrieve embeddings for machine learning models

+
+
+

💡 AI-Enhanced Applications

+

Build applications that leverage vector embeddings for intelligent data processing

+
+
+

📁 Data Organization Tools

+

Automatically categorize and connect related information

+
+
+

🔄 Adaptive Experiences

+

Create applications that learn and evolve with your users

+
+
+

🔌 Model-Integrated Systems

+

Connect external AI models to Brainy data and tools using MCP

+
+
+
+ +
+

Interactive Brainy Explorer

+

This interactive demo guides you through Brainy's features step by step, from basic operations to advanced + capabilities.

+ + +
+

Step 1: Getting Started with Brainy

+

Let's start by initializing the database and exploring basic operations.

+ +
+ + + + + +
+ +
+ Database not initialized. Click "Initialize Database" to start. +
+ +
+
+ +
+
+
+ Database visualization will appear here after initialization +
+
+
+
+ + +
+

Step 2: Semantic Search

+

Search your data using vector embeddings for semantic understanding.

+ + +
+
+ +
+ +
+
+ +
+
+ + +
+
+
+ +

+ Try searching for topics in the sample data like "travel", "AI", "fitness", "food", "design", + "sustainability", or specific concepts like "workout challenge", "social media", "photography", etc. +

+ +
+
+ Search results will appear here... +
+
+ +
+
+ + + +
+
+
+ Search results visualization +
+
+
+
+ + +
+

Step 3: Adding Entities (Nouns)

+

Now that your database is initialized, let's add some entities (nouns) to it.

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ Entity operations will appear here... +
+
+ + +
+

Step 4: Creating Relationships (Verbs)

+

Connect your entities with meaningful relationships to build a knowledge graph.

+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+

Step 5: Advanced Features

+

Explore Brainy's advanced capabilities for more complex use cases.

+ +
+
+

🔍 Vector Search

+

Explore how HNSW makes semantic search fast and efficient.

+ +
+ +
+

🌐 Graph Traversal

+

Navigate through connected entities in your knowledge graph.

+ +
+ +
+

💾 Data Management

+

Backup, restore and manage your data.

+
+ + +
+
+ +
+

🧠 Embedding Pipeline

+

See how text is converted to vectors for semantic understanding.

+ +
+
+ +
+ Select a feature above to see a detailed demonstration... +
+
+ + +
+

Step 6: Command Line Interface

+

Try Brainy's CLI commands for scripting and automation.

+ +
+
+
+ brainy$ + +
+
+
+ +
+ + + + + +
+ +
+ Common Commands:
+ brainy init - Initialize a new database
+ brainy add <data> - Add data to the database
+ brainy search <query> - Search for entities
+ brainy relate <from> <to> - Create relationships
+ brainy status - Show database status
+ brainy help - Show all commands +
+
+
+ + +
+

🏁 Quick Start

+ +
+
+

Node.js Environment

+
+import { BrainyData, NounType, VerbType } from '@soulcraft/brainy'
+
+// Create and initialize the database
+const db = new BrainyData()
+await db.init()
+
+// Add data (automatically converted to vectors)
+const catId = await db.add("Cats are independent pets", {
+    noun: NounType.Thing,
+    category: 'animal'
+})
+
+const dogId = await db.add("Dogs are loyal companions", {
+    noun: NounType.Thing,
+    category: 'animal'
+})
+
+// Search for similar items
+const results = await db.searchText("feline pets", 2)
+console.log(results)
+// Returns items similar to "feline pets" with similarity scores
+
+// Add a relationship between items
+await db.addVerb(catId, dogId, {
+    verb: VerbType.RelatedTo,
+    description: 'Both are common household pets'
+})
+        
+
+ +
+

Browser Environment

+

Brainy uses a unified build that automatically adapts to your environment:

+ +

Option 1: Standard Import

+
+// Standard import - automatically adapts to any environment
+import { BrainyData, NounType, VerbType } from '@soulcraft/brainy'
+
+// Use the same API as in Node.js
+const db = new BrainyData()
+await db.init()
+// ...
+        
+ +

Option 2: Using a script tag in HTML

+
+<script type="module">
+  // Use local files instead of CDN
+  // For GitHub Pages, use relative path: './dist/unified.js'
+  // For local development, use absolute path: '/dist/unified.js'
+  // This import is replaced by the dynamic import below
+
+  // Or minified version
+  // import { BrainyData, NounType, VerbType } from '/dist/unified.min.js'
+
+  const db = new BrainyData()
+  await db.init()
+  // ...
+</script>
+        
+

Modern bundlers like Webpack, Rollup and Vite will automatically use the unified build which adapts to any + environment.

+
+
+
+ + +