From 7b53e80217839e2be8f18ea9bb204316796208e1 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Mon, 23 Jun 2025 14:46:26 -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 | 44 + .gitignore | 41 + .npmignore | 40 + CODE_OF_CONDUCT.md | 128 + CONTRIBUTING.md | 89 + LICENSE | 21 + README.demo.md | 59 + README.md | 1257 +++ brainy.png | Bin 0 -> 222935 bytes clean-history.sh | 55 + 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 | 4240 +++++++++ encoded-image.html | 1 + encoded-image.txt | 1 + index.html | 17 + package-lock.json | 8315 +++++++++++++++++ package.json | 156 + rollup.config.js | 179 + scripts/encode-image.js | 47 + scripts/generate-version.js | 74 + 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 | 702 ++ 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 | 284 + src/utils/environment.ts | 57 + src/utils/index.ts | 2 + src/utils/version.ts | 6 + src/utils/workerUtils.ts | 137 + tsconfig.browser.json | 30 + tsconfig.json | 29 + tsconfig.unified.json | 29 + verify-package-size.js | 135 + 81 files changed, 39132 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 clean-history.sh 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 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..cb91fde1 --- /dev/null +++ b/.github/workflows/deploy-demo.yml @@ -0,0 +1,44 @@ +name: Deploy Demo to GitHub Pages + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v3 + + - 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 deploy + mkdir -p deploy/demo + cp index.html deploy/ + cp demo/index.html deploy/demo/ + cp -r dist deploy/ + cp brainy.png deploy/ + + - name: Deploy to GitHub Pages 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: deploy + branch: gh-pages diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6fa7bc6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# 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 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..ebbb2db6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +
+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 +Add tests for metadata filtering +Update build dependencies +``` + +## Pull Request Process + +1. Ensure your code follows the project's coding standards +2. Update the documentation if necessary +3. Make sure all tests pass +4. Use conventional commit messages in your PR +5. 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` +4. Run tests: `npm test` + +## 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..67e1c663 --- /dev/null +++ b/README.md @@ -0,0 +1,1257 @@ +
+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) +[![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 + - `npm run deploy:demo`: Deploys the examples directory to GitHub Pages + +### 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: + +- `examples/demo.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, run `npm run deploy:demo` + - To deploy automatically when pushing to the main branch, a GitHub Actions workflow is included in + `.github/workflows/deploy-demo.yml` + - 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() +``` + +For a complete demonstration of Brainy's features, see the [demo page](index.html). + +## 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. Run tests with `npm test` +5. Submit a pull request + +## 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..3ebc1de7 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,4240 @@ + + + + + + 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..ffdbe268 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8315 @@ +{ + "name": "@soulcraft/brainy", + "version": "0.9.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@soulcraft/brainy", + "version": "0.9.4", + "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..88a68f6d --- /dev/null +++ b/package.json @@ -0,0 +1,156 @@ +{ + "name": "@soulcraft/brainy", + "version": "0.9.4", + "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: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/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", + "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" + }, + "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/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..2a462529 --- /dev/null +++ b/scripts/generate-version.js @@ -0,0 +1,74 @@ +#!/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 +try { + let readmeContent = fs.readFileSync(readmePath, 'utf8') + + // Replace the version in the badge URL + const updatedReadme = readmeContent.replace( + /\[\!\[Version\]\(https:\/\/img\.shields\.io\/badge\/version-[0-9]+\.[0-9]+\.[0-9]+-blue\.svg\)\]/g, + `[![Version](https://img.shields.io/badge/version-${version}-blue.svg)]` + ) + + // Write the updated README back to disk + fs.writeFileSync(readmePath, updatedReadme) + console.log(`Updated README.md with version ${version}`) +} 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..bf46f1d5 --- /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, ...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..f489fcf9 --- /dev/null +++ b/src/hnsw/hnswIndex.ts @@ -0,0 +1,702 @@ +/** + * 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 = ( + query: Vector, + items: Array<{ id: string; vector: Vector }>, + distanceFn: string + ) => { + // Recreate the distance function from its string representation + const distanceFunction = new Function('return ' + distanceFn)() as DistanceFunction + + // Calculate distances for all items + return items.map(item => ({ + id: item.id, + distance: distanceFunction(query, 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, + 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..e27441c8 --- /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, ...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..6e15e204 --- /dev/null +++ b/src/utils/embedding.ts @@ -0,0 +1,284 @@ +/** + * 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 + const embedInWorker = async (inputData: 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 inputData === 'string') { + if (inputData.trim() === '') { + return new Array(512).fill(0) + } + textToEmbed = [inputData] + } else if ( + Array.isArray(inputData) && + inputData.every((item) => typeof item === 'string') + ) { + if (inputData.length === 0 || inputData.every((item) => item.trim() === '')) { + return new Array(512).fill(0) + } + textToEmbed = inputData.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 + return await executeInThread(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..3a7c9950 --- /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.4'; diff --git a/src/utils/workerUtils.ts b/src/utils/workerUtils.ts new file mode 100644 index 00000000..8e500ee3 --- /dev/null +++ b/src/utils/workerUtils.ts @@ -0,0 +1,137 @@ +/** + * 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 fn The function to execute + * @param args The arguments to pass to the function + * @returns A promise that resolves with the result of the function + */ +export function executeInWebWorker(fn: Function, ...args: any[]): Promise { + return new Promise((resolve, reject) => { + // Create a blob URL for the worker script + const fnString = fn.toString(); + const workerScript = ` + self.onmessage = function(e) { + try { + const fn = ${fnString}; + const result = 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 fn The function to execute + * @param args The arguments to pass to the function + * @returns A promise that resolves with the result of the function + */ +export function executeInWorkerThread(fn: Function, ...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 fnString = fn.toString(); + const workerScript = ` + const { parentPort } = require('worker_threads'); + + parentPort.once('message', (data) => { + try { + const fn = ${fnString}; + const result = 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 fn The function to execute + * @param args The arguments to pass to the function + * @returns A promise that resolves with the result of the function + */ +export function executeInThread(fn: Function, ...args: any[]): Promise { + if (isBrowser()) { + return executeInWebWorker(fn, ...args); + } else if (isNode()) { + return executeInWorkerThread(fn, ...args); + } else { + // Fall back to executing in the main thread + 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
+  import { BrainyData, NounType, VerbType } from '../dist/unified.js'
+
+  // 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.

+
+
+
+ + +