diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d758f546 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,307 @@ +root = true + +# Keep LF line endings in generated web assets to avoid CI asset validation diffs. +# This aligns with `.gitattributes` (wwwroot/**/*.js|css => eol=lf). +[**/wwwroot/**/*.js] +end_of_line = lf + +[**/wwwroot/**/*.css] +end_of_line = lf + +[*] +end_of_line = crlf +charset = utf-8 +indent_style = space +indent_size = 4 + +[*.{json,yml,csproj,props,targets}] +indent_size = 2 + +[*.cs] +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true : suggestion +csharp_style_var_when_type_is_apparent = true : suggestion +csharp_style_var_elsewhere = true : suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +dotnet_diagnostic.IDE0290.severity = none +dotnet_diagnostic.IDE0305.severity = none + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = file_scoped:suggestion + +# Range operator +csharp_style_prefer_range_operator = false:warning +csharp_style_prefer_index_operator = false:warning +dotnet_diagnostic.IDE0057.severity = warning +dotnet_diagnostic.IDE0056.severity = warning + +# Avoid "this." if not necessary +dotnet_style_qualification_for_field = false : suggestion +dotnet_style_qualification_for_property = false : suggestion +dotnet_style_qualification_for_method = false : suggestion +dotnet_style_qualification_for_event = false : suggestion + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true : suggestion +dotnet_style_predefined_type_for_member_access = true : suggestion + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true : none +csharp_style_pattern_matching_over_as_with_null_check = true : none +csharp_style_inlined_variable_declaration = true : none +csharp_style_throw_expression = true : none +csharp_style_conditional_delegate_call = true : none + +dotnet_style_object_initializer = true : suggestion +dotnet_style_collection_initializer = true : suggestion +dotnet_style_coalesce_expression = true : suggestion +dotnet_style_null_propagation = true : suggestion +dotnet_style_explicit_tuple_names = true : suggestion + +trim_trailing_whitespace = true +insert_final_newline = true + +# Suggest to use "_" instead of "this." when creating new private fields. +dotnet_naming_style.camel_case_leading_underscore.capitalization = camel_case +dotnet_naming_style.camel_case_leading_underscore.required_prefix = _ + +# Define the 'private_fields' symbol group. +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +# Define the 'private_fields_should_be_camel_case_leading_underscore' naming rule. +dotnet_naming_rule.private_fields_should_be_camel_case_leading_underscore.severity = suggestion +dotnet_naming_rule.private_fields_should_be_camel_case_leading_underscore.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_camel_case_leading_underscore.style = camel_case_leading_underscore + +# Define the 'private_const_fields' symbol group. +dotnet_naming_symbols.private_const_fields.applicable_kinds = field +dotnet_naming_symbols.private_const_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_const_fields.required_modifiers = const + +# Define the 'private_const_fields_ignore_camel_case_leading_underscore' naming rule. +dotnet_naming_rule.private_const_fields_ignore_camel_case_leading_underscore.severity = none +dotnet_naming_rule.private_const_fields_ignore_camel_case_leading_underscore.symbols = private_const_fields +dotnet_naming_rule.private_const_fields_ignore_camel_case_leading_underscore.style = camel_case_leading_underscore + +# StyleCop +# Rules are listed at https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/rulesets/StyleCopAnalyzersDefault.ruleset + +# StyleCop: Spacing Rules +[*.cs] +dotnet_diagnostic.SA0001.severity = none +dotnet_diagnostic.SA1000.severity = none # Keywords should be spaced correctly. +dotnet_diagnostic.SA1001.severity = none # Commas should be spaced correctly. +dotnet_diagnostic.SA1002.severity = none # Semicolons should be spaced correctly. +dotnet_diagnostic.SA1003.severity = suggestion # Symbols should be spaced correctly. +dotnet_diagnostic.SA1004.severity = suggestion # Documentation lines should begin with single space. +dotnet_diagnostic.SA1005.severity = suggestion # Single line comment should begin with a space. +dotnet_diagnostic.SA1006.severity = none # Preprocessor keywords should not be preceded by space. +dotnet_diagnostic.SA1007.severity = none # Operator keyword should be followed by space. +dotnet_diagnostic.SA1008.severity = none # Opening parenthesis should not be followed by a space. +dotnet_diagnostic.SA1009.severity = none # Closing parenthesis should be spaced correctly. +dotnet_diagnostic.SA1010.severity = none # Opening square brackets should be spaced correctly. +dotnet_diagnostic.SA1011.severity = none # Closing square brackets should be spaced correctly. +dotnet_diagnostic.SA1012.severity = none # Opening braces should be spaced correctly. +dotnet_diagnostic.SA1013.severity = none # Closing braces should be spaced correctly. +dotnet_diagnostic.SA1014.severity = none # Opening generic brackets should be spaced correctly. +dotnet_diagnostic.SA1015.severity = none # Closing generic brackets should be spaced correctly. +dotnet_diagnostic.SA1016.severity = none # Opening attribute brackets should be spaced correctly. +dotnet_diagnostic.SA1017.severity = none # Closing attribute brackets should be spaced correctly. +dotnet_diagnostic.SA1018.severity = none # Nullable type symbols should be spaced correctly. +dotnet_diagnostic.SA1019.severity = none # Member access symbols should be spaced correctly. +dotnet_diagnostic.SA1020.severity = none # Increment decrement symbols should be spaced correctly. +dotnet_diagnostic.SA1021.severity = none # Negative signs should be spaced correctly. +dotnet_diagnostic.SA1022.severity = none # Positive signs should be spaced correctly. +dotnet_diagnostic.SA1023.severity = none # Dereference and access of symbols should be spaced correctly. +dotnet_diagnostic.SA1024.severity = none # Colons should be spaced correctly. +dotnet_diagnostic.SA1025.severity = none # Code should not contain multiple whitespace in a row +dotnet_diagnostic.SA1026.severity = none # The keyword 'new' should not be followed by a space or a blank line. +dotnet_diagnostic.SA1027.severity = none # Use tabs correctly +dotnet_diagnostic.SA1028.severity = none # Code should not contain trailing whitespace. + +# StyleCop: Readability Rules +dotnet_diagnostic.SA1100.severity = none # Do not prefix calls with base unless local implementation exists +dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this. +dotnet_diagnostic.SA1102.severity = none # Query clause should follow previous clause. +dotnet_diagnostic.SA1103.severity = none # Query clauses should be on separate lines or all on one line. +dotnet_diagnostic.SA1104.severity = none # Query clause should begin on new line when previous clause spans multiple lines. +dotnet_diagnostic.SA1105.severity = none # Query clauses spanning multiple lines should begin on own line. +dotnet_diagnostic.SA1106.severity = none # Code should not contain empty statements. +dotnet_diagnostic.SA1107.severity = none # Code should not contain multiple statements on one line. +dotnet_diagnostic.SA1108.severity = none # Block statements should not contain embedded comments. +dotnet_diagnostic.SA1109.severity = none # Block statements should not contain embedded regions. +dotnet_diagnostic.SA1110.severity = none # Opening parenthesis or bracket should be on declaration line. +dotnet_diagnostic.SA1111.severity = none # Closing parenthesis should be on line of last parameter. +dotnet_diagnostic.SA1112.severity = none # Closing parenthesis should be on line of opening parenthesis. +dotnet_diagnostic.SA1113.severity = none # Comma should be on the same line as previous parameter. +dotnet_diagnostic.SA1114.severity = none # Parameter list should follow declaration. +dotnet_diagnostic.SA1115.severity = none # Parameter should follow comma . +dotnet_diagnostic.SA1116.severity = none # Split parameters should start on line after declaration. +dotnet_diagnostic.SA1117.severity = none # Parameters should be on same line or separate lines. +dotnet_diagnostic.SA1118.severity = none # The parameter spans multiple lines. +dotnet_diagnostic.SA1119.severity = none # Statement should not use unnecessary parenthesis. +dotnet_diagnostic.SA1120.severity = none # Comments should contain text. +dotnet_diagnostic.SA1121.severity = none # Use built-in type alias +dotnet_diagnostic.SA1122.severity = none # Use string.Empty for empty strings. +dotnet_diagnostic.SA1124.severity = none # Do not use regions. +dotnet_diagnostic.SA1125.severity = none # Use shorthand for nullable types. +dotnet_diagnostic.SA1126.severity = none # Prefix calls correctly. +dotnet_diagnostic.SA1127.severity = none # Generic type constraints should be on their own line. +dotnet_diagnostic.SA1128.severity = none # Generic type constraints should be on their own line. +dotnet_diagnostic.SA1129.severity = none # Do not use default value type constructor. +dotnet_diagnostic.SA1130.severity = none # Use lambda syntax. +dotnet_diagnostic.SA1131.severity = none # Use readable conditions. +dotnet_diagnostic.SA1132.severity = none # Each field should be declared on its own line +dotnet_diagnostic.SA1133.severity = none # Do not combine attributes. +dotnet_diagnostic.SA1134.severity = none # Attributes should not share line. +dotnet_diagnostic.SA1135.severity = none # Using directives should be qualified. +dotnet_diagnostic.SA1136.severity = none # Enum values should be on separate lines. +dotnet_diagnostic.SA1137.severity = none # Elements should have the same indentation. +dotnet_diagnostic.SA1138.severity = none # Indent elements correctly. +dotnet_diagnostic.SA1139.severity = none # Use literal suffix notation instead of casting. + +# StyleCop: Ordering Rules +dotnet_diagnostic.SA1200.severity = none # Using directives should be placed correctly. +dotnet_diagnostic.SA1201.severity = none # Elements should appear in the correct order. +dotnet_diagnostic.SA1202.severity = none # Elements should be ordered by access. +dotnet_diagnostic.SA1203.severity = none # Constants should appear before fields. +dotnet_diagnostic.SA1204.severity = none # Static elements should appear before instance elements. +dotnet_diagnostic.SA1205.severity = none # Partial elements should declare access. +dotnet_diagnostic.SA1206.severity = none # Declaration keywords should follow order. +dotnet_diagnostic.SA1207.severity = none # Protected should come before internal. +dotnet_diagnostic.SA1208.severity = none # System using directives should be placed before other using directives. +dotnet_diagnostic.SA1209.severity = none # Using alias directives should be placed after other using directives. +dotnet_diagnostic.SA1210.severity = none # Using directives should be ordered alphabetically by namespace. +dotnet_diagnostic.SA1211.severity = none # Using alias directives should be ordered alphabetically by alias name. +dotnet_diagnostic.SA1212.severity = none # Property accessors should follow order. +dotnet_diagnostic.SA1213.severity = none # Event accessors should follow order. +dotnet_diagnostic.SA1214.severity = none # Readonly fields should appear before non-readonly fields. +dotnet_diagnostic.SA1216.severity = none # Using static directives should be placed at the correct location. +dotnet_diagnostic.SA1217.severity = none # Using static directives should be ordered alphabetically. + +# StyleCop: Naming Rules +dotnet_diagnostic.SA1300.severity = none # Element 'requestDelegate' should begin with an uppercase letter. +dotnet_diagnostic.SA1301.severity = none # Element should begin with lower-case letter. +dotnet_diagnostic.SA1302.severity = none # Interface names should begin with I. +dotnet_diagnostic.SA1303.severity = none # Const field names should begin with upper-case letter. +dotnet_diagnostic.SA1304.severity = none # Non-private readonly fields should begin with upper-case letter. +dotnet_diagnostic.SA1305.severity = none # Field names should not use Hungarian notation. +dotnet_diagnostic.SA1306.severity = none # Field names should begin with lower-case letter. +dotnet_diagnostic.SA1307.severity = none # Accessible fields should begin with upper-case letter. +dotnet_diagnostic.SA1308.severity = none # Variable names should not be prefixed. +dotnet_diagnostic.SA1309.severity = none # Field names should not begin with underscore. +dotnet_diagnostic.SA1310.severity = none # Field names should not contain underscore. +dotnet_diagnostic.SA1311.severity = none # Static readonly fields should begin with upper-case letter. +dotnet_diagnostic.SA1312.severity = none # Variable '_' should begin with lower-case letter. +dotnet_diagnostic.SA1313.severity = none # Parameter '_' should begin with lower-case letter. +dotnet_diagnostic.SA1314.severity = none # Type parameter names should begin with T. + +# StyleCop: Maintainability Rules +dotnet_diagnostic.SA1400.severity = none # Access modifier should be declared. +dotnet_diagnostic.SA1401.severity = none # Fields should be private. +dotnet_diagnostic.SA1402.severity = none # File may only contain a single type. +dotnet_diagnostic.SA1403.severity = none # File may only contain a single namespace. +dotnet_diagnostic.SA1404.severity = none # Code analysis suppression should have justification. +dotnet_diagnostic.SA1405.severity = none # Debug.Assert should provide message text. +dotnet_diagnostic.SA1406.severity = none # Debug.Fail should provide message text. +dotnet_diagnostic.SA1407.severity = none # Arithmetic expressions should declare precedence. +dotnet_diagnostic.SA1408.severity = none # Conditional expressions should declare precedence. +dotnet_diagnostic.SA1409.severity = none # Remove unnecessary code. +dotnet_diagnostic.SA1410.severity = none # Remove delegate parenthesis when possible. +dotnet_diagnostic.SA1411.severity = none # Attribute constructor should not use unnecessary parenthesis. +dotnet_diagnostic.SA1412.severity = none # Store files as UTF-8 with byte order mark. +dotnet_diagnostic.SA1413.severity = none # Use trailing comma in multi-line initializers. + +# StyleCop: Layout Rules +dotnet_diagnostic.SA1500.severity = none # Braces for multi-line statements should not share line. +dotnet_diagnostic.SA1501.severity = none # Statement should not be on a single line. +dotnet_diagnostic.SA1502.severity = none # Element should not be on a single line. +dotnet_diagnostic.SA1503.severity = none # Braces should not be omitted. +dotnet_diagnostic.SA1504.severity = none # All accessors should be single-line or multi-line. +dotnet_diagnostic.SA1505.severity = none # A closing brace should not be preceded by a blank line. +dotnet_diagnostic.SA1506.severity = none # Element documentation headers should not be followed by blank line. +dotnet_diagnostic.SA1507.severity = none # Code should not contain multiple blank lines in a row. +dotnet_diagnostic.SA1508.severity = none # Closing braces should not be preceded by blank line. +dotnet_diagnostic.SA1509.severity = none # Opening braces should not be preceded by blank line. +dotnet_diagnostic.SA1510.severity = none # Chained statement blocks should not be preceded by blank line. +dotnet_diagnostic.SA1511.severity = none # While-do footer should not be preceded by blank line. +dotnet_diagnostic.SA1512.severity = none # Single-line comments should not be followed by blank line. +dotnet_diagnostic.SA1513.severity = none # Closing brace should be followed by blank line. +dotnet_diagnostic.SA1514.severity = none # Element documentation header should be preceded by blank line. +dotnet_diagnostic.SA1515.severity = none # Single-line comment should be preceded by blank line. +dotnet_diagnostic.SA1516.severity = none # Elements should be separated by blank line. +dotnet_diagnostic.SA1517.severity = none # Code should not contain blank lines at start of file. +dotnet_diagnostic.SA1518.severity = none # Use line endings correctly at end of file. +dotnet_diagnostic.SA1519.severity = none # Braces should not be omitted from multi-line child statement. +dotnet_diagnostic.SA1520.severity = none # Use braces consistently. + +# StyleCop: Documentation Rules +dotnet_diagnostic.SA1600.severity = none # Elements should be documented. +dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented. +dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented. +dotnet_diagnostic.SA1603.severity = none # Documentation should contain valid XML. +dotnet_diagnostic.SA1604.severity = none # Element documentation should have summary. +dotnet_diagnostic.SA1605.severity = none # Partial element documentation should have summary. +dotnet_diagnostic.SA1606.severity = none # Element documentation should have summary text. +dotnet_diagnostic.SA1607.severity = none # Partial element documentation should have summary text. +dotnet_diagnostic.SA1608.severity = none # Element documentation should not have default summary. +dotnet_diagnostic.SA1609.severity = none # Property documentation should have value. +dotnet_diagnostic.SA1610.severity = none # Property documentation should have value text. +dotnet_diagnostic.SA1611.severity = none # Element parameters should be documented. +dotnet_diagnostic.SA1612.severity = none # Element parameter documentation should match element parameters. +dotnet_diagnostic.SA1613.severity = none # Element parameter documentation should declare parameter name. +dotnet_diagnostic.SA1614.severity = none # Element parameter documentation should have text. +dotnet_diagnostic.SA1615.severity = none # Element return value should be documented. +dotnet_diagnostic.SA1616.severity = none # Element return value documentation should have text. +dotnet_diagnostic.SA1617.severity = none # Void return value should not be documented. +dotnet_diagnostic.SA1618.severity = none # The documentation for type parameter 'T' is missing. +dotnet_diagnostic.SA1619.severity = none # Generic type parameters should be documented partial class. +dotnet_diagnostic.SA1620.severity = none # Generic type parameter documentation should match type parameters. +dotnet_diagnostic.SA1621.severity = none # Generic type parameter documentation should declare parameter name. +dotnet_diagnostic.SA1622.severity = none # Generic type parameter documentation should have text. +dotnet_diagnostic.SA1623.severity = none # Property summary documentation should match accessors. +dotnet_diagnostic.SA1624.severity = none # Property summary documentation should omit accessor with restricted access. +dotnet_diagnostic.SA1625.severity = none # Element documentation should not be copied and pasted. +dotnet_diagnostic.SA1626.severity = none # Single-line comments should not use documentation style slashes. +dotnet_diagnostic.SA1627.severity = none # Documentation text should not be empty. +dotnet_diagnostic.SA1628.severity = none # Documentation text should begin with a capital letter. +dotnet_diagnostic.SA1629.severity = suggestion # Documentation text should end with a period. +dotnet_diagnostic.SA1630.severity = none # Documentation text should contain whitespace. +dotnet_diagnostic.SA1631.severity = none # Documentation should meet character percentage. +dotnet_diagnostic.SA1632.severity = none # Documentation text should meet minimum character length. +dotnet_diagnostic.SA1633.severity = none # File should have header. +dotnet_diagnostic.SA1634.severity = none # File header should show copyright. +dotnet_diagnostic.SA1635.severity = none # File header should have copyright text. +dotnet_diagnostic.SA1636.severity = none # File header copyright text should match. +dotnet_diagnostic.SA1137.severity = none # File header should contain file name. +dotnet_diagnostic.SA1638.severity = none # File header file name documentation should match file name. +dotnet_diagnostic.SA1639.severity = none # File header should have summary. +dotnet_diagnostic.SA1640.severity = none # File header should have valid company text. +dotnet_diagnostic.SA1641.severity = none # File header company name text should match. +dotnet_diagnostic.SA1642.severity = none # Constructor summary documentation should begin with standard text. +dotnet_diagnostic.SA1643.severity = none # Destructor summary documentation should begin with standard text. +dotnet_diagnostic.SA1644.severity = none # Documentation headers should not contain blank lines. +dotnet_diagnostic.SA1645.severity = none # Included documentation file does not exist. +dotnet_diagnostic.SA1646.severity = none # Included documentation XPath does not exist. +dotnet_diagnostic.SA1647.severity = none # Include node does not contain valid file and path. +dotnet_diagnostic.SA1648.severity = none # Inheritdoc should be used with inheriting class. +dotnet_diagnostic.SA1649.severity = none # File name should match first type name. +dotnet_diagnostic.SA1650.severity = none # Element documentation should be spelled correctly. +dotnet_diagnostic.SA1651.severity = none # Do not use placeholder elements. + +# IDE0290: Use primary constructor +dotnet_diagnostic.IDE0290.severity = none \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..b175f24b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto + +**/wwwroot/Scripts/* linguist-vendored + +# Keep LF line endings in webroot assets files. Otherwise, building them under Windows would change the line endings to CLRF and cause changes without actually editing the source files. +**/wwwroot/**/*.js text eol=lf +**/wwwroot/**/*.css text eol=lf diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..f9ae8170 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing to CrestApps.Core + +Thanks for contributing to `CrestApps.Core`. This repository contains the shared CrestApps framework libraries, sample hosts, and the documentation site. + +## Getting started + +```bash +git clone https://github.com/CrestApps/CrestApps.Core.git +cd CrestApps.Core +git checkout main +``` + +Install: + +- .NET 10 SDK +- Node.js 20+ for the docs site and asset tooling + +Common commands: + +```bash +dotnet build .\CrestApps.Core.slnx -c Release /p:NuGetAudit=false +dotnet test .\tests\CrestApps.Core.Tests\CrestApps.Core.Tests.csproj -c Release /p:NuGetAudit=false +cd src\CrestApps.Core.Docs +npm install +npm run build +``` + +## What to work on + +- Browse [open issues](https://github.com/CrestApps/CrestApps.Core/issues) +- For larger features or behavioral changes, open an issue before you start + +## Pull request expectations + +- Keep changes focused +- Add or update tests when behavior changes +- Update the Docusaurus docs in `src\CrestApps.Core.Docs` when user-facing behavior or project guidance changes +- Include a changelog update when the change is release-note worthy +- Link the PR to the related issue when applicable +- Mark the PR as draft if it is not ready for review + +## Review workflow + +- Address review feedback in follow-up commits +- Do not manually resolve review conversations +- Re-request review after feedback is addressed + +## Related documentation + +- Core framework docs: +- Orchard Core implementation docs: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..b9813ae0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a bug report to help us improve +title: '' +labels: 'bug :bug:' +assignees: '' + +--- + +### Describe the bug + +### CrestApps.Core version + +Add the package version or commit hash you are using. + +### To Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +### Expected behavior +A clear and concise description of what you expected to happen. + +### Logs and screenshots +If applicable, add log files, browser console logs, and screenshots (or screen recording videos) to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..b5c2c386 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Discussions + url: https://github.com/CrestApps/CrestApps.Core/discussions + about: Use GitHub Discussions for questions, ideas, and broader design conversations. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..67f097aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + + +### Is your feature request related to a problem? Please describe. +A clear and concise description of what the problem is. + +### Describe the solution you'd like +A clear and concise description of what you want to happen. + +### Describe alternatives you've considered +A clear and concise description of any alternative solutions or features you've considered. diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md new file mode 100644 index 00000000..ce66be6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.md @@ -0,0 +1,34 @@ +--- +name: Publish a release +about: Publish a new CrestApps.Core release +title: 'Release v' +labels: release +assignees: '' + +--- +Use this checklist for a framework release such as `v1.0.0`. + +### Prepare the release + +- [ ] Confirm the milestone and release issue are up to date. +- [ ] Make sure `Directory.Build.props` has the intended `VersionPrefix` and `VersionSuffix` values. +- [ ] Update the docs changelog under `src/CrestApps.Core.Docs/docs/changelog/`. +- [ ] Review README, package metadata, and release-facing docs for version-specific text. + +### Validate the release + +- [ ] Run the full build in Release mode. +- [ ] Run the test suite. +- [ ] Build the docs site. +- [ ] Smoke test the MVC sample host if the release changes runtime behavior. + +### Publish the release + +- [ ] Tag the release as `v`. +- [ ] Let `release_ci.yml` publish the NuGet packages from the tag. +- [ ] Create the GitHub release and link to the docs release notes. + +### After publishing + +- [ ] Add or update the next changelog entry under `docs/changelog/`. +- [ ] Move the next development cycle back to preview if needed. diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000..fac32fb7 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,40 @@ +name: "CodeQL Config for CrestApps.Core" + +# This configuration file is referenced by .github/workflows/codeql.yml. +# It is NOT automatically used by GitHub's default CodeQL setup. + +# Paths to ignore during analysis. +paths-ignore: + - node_modules + - "**/wwwroot/**" + - "**/bin/**" + - "**/obj/**" + - "docs/**" + - "BenchmarkDotNet.Artifacts/**" + - "TestResults/**" + # Third-party / generated files that should not be analysed. + - "**/Assets/js/flatpickr-culture.js" + +query-filters: + # Exclude CSRF (missing antiforgery token) alerts. + # OrchardCore framework automatically validates antiforgery tokens for all + # POST requests via its ModularApplicationModelProvider. Every module in this + # repository references OrchardCore.Module.Targets, which enables this global + # validation. Adding [ValidateAntiForgeryToken] to individual actions is + # therefore redundant. + - exclude: + id: cs/web/missing-token-validation + - exclude: + id: cs/web/missing-csrf-token-validation + + # Exclude XSS alerts for ASP.NET Core tag helper attributes. + # All flagged views are admin-only (behind [Admin] attribute requiring + # authentication) and use ASP.NET Core tag helper attributes like + # asp-route-id="@ViewContext.HttpContext.Request.RouteValues["id"]". Tag + # helpers process these values server-side and generate properly URL-encoded + # action URLs — the asp-route-* attribute itself is never rendered to HTML. + # Razor also auto-encodes @ expressions. CodeQL cannot model tag helper + # encoding as a sanitizer, producing false positives for this standard + # OrchardCore admin pattern. + - exclude: + id: cs/web/xss diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..53cd2304 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,83 @@ +# CrestApps.Core Development Instructions + +Use these instructions when working in this repository. + +## Project overview + +`CrestApps.Core` is the standalone framework repository for CrestApps shared libraries. It contains reusable .NET packages for AI, orchestration, chat, templating, document processing, SignalR, storage, and sample hosts. + +- **Target framework:** .NET 10 +- **Docs site:** `src\CrestApps.Core.Docs` +- **Sample hosts:** `src\Startup\CrestApps.Core.Mvc.Web`, `src\Startup\CrestApps.Core.Aspire.AppHost` +- **Tests:** `tests\CrestApps.Core.Tests` + +Orchard Core-specific implementation details belong to the separate site at . + +## Build and test + +Use the repository root unless a command says otherwise. + +```bash +dotnet build .\CrestApps.Core.slnx -c Release /p:NuGetAudit=false +dotnet test .\tests\CrestApps.Core.Tests\CrestApps.Core.Tests.csproj -c Release /p:NuGetAudit=false +``` + +For root asset tooling: + +```bash +npm install +npm run rebuild +``` + +For the docs site: + +```bash +cd src\CrestApps.Core.Docs +npm install +npm run build +``` + +## Repository layout + +Important folders: + +- `src\Abstractions` - public contracts +- `src\Primitives` - concrete framework features and providers +- `src\Stores` - persistence implementations +- `src\Utilities` - shared helpers +- `src\Startup` - runnable sample hosts +- `src\CrestApps.Core.Docs` - Docusaurus docs +- `tests\CrestApps.Core.Tests` - unit tests + +## Documentation expectations + +When a change affects public behavior, configuration, setup, or project guidance: + +1. Update the relevant page under `src\CrestApps.Core.Docs\docs` +2. Update the changelog under `src\CrestApps.Core.Docs\docs\changelog` +3. Build the docs site + +Keep the docs focused on `CrestApps.Core`. If you need to mention the Orchard Core implementation, treat it as a related downstream product and link to . + +## Coding guidance + +- Follow `.editorconfig` +- Prefer constructor injection +- Use `var` consistently with repository style +- Only use expression-bodied members when the entire member fits on a single short line; use a full block body for anything longer or split across lines +- Avoid `DateTime.UtcNow`; prefer injected time abstractions when the surrounding code already uses them +- Keep public docs and comments honest to the current code + +## Runtime notes + +Use the MVC sample host when you need to inspect end-to-end framework behavior: + +```bash +dotnet run --project .\src\Startup\CrestApps.Core.Mvc.Web\CrestApps.Core.Mvc.Web.csproj +``` + +Use the Aspire host when you need the composed local environment: + +```bash +dotnet run --project .\src\Startup\CrestApps.Core.Aspire.AppHost\CrestApps.Core.Aspire.AppHost.csproj +``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..876330a9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" + # Disable version update PRs; only Dependabot security updates create PRs. + open-pull-requests-limit: 0 + ignore: + - dependency-name: "OrchardCore.*" + - dependency-name: "SafeMode" + - dependency-name: "TheAdmin" + - dependency-name: "TheAgencyTheme" + - dependency-name: "TheBlogTheme" + - dependency-name: "TheComingSoonTheme" + - dependency-name: "TheTheme" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 0 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 0 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..3e387368 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/workflows/assets_validation.yml b/.github/workflows/assets_validation.yml new file mode 100644 index 00000000..bf234820 --- /dev/null +++ b/.github/workflows/assets_validation.yml @@ -0,0 +1,124 @@ +name: Validating the Building of Public Assets + +on: + # Run it on main and release pushes too, in case we merge from a branch that's not up-to-date with the target branch + # and breaks something after merge (or if we push to main). + push: + paths-ignore: + - '**/*.md' + - 'mkdocs.yml' + - 'src/CrestApps.Core.Docs/**' + branches: [ main, release/** ] + pull_request: + branches: [ main, release/** ] + +permissions: + contents: read + +concurrency: + group: ${{ github.head_ref || github.run_id }}-assets_validation + cancel-in-progress: true + +jobs: + test-npm-build: + name: Test building assets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Rebuild packages + run: | + npm ci + gulp rebuild + - name: Normalize line endings (gitattributes) + run: | + git add --renormalize . + git reset --quiet + - name: Check if git has changes + shell: pwsh + run: | + $changes = git status --porcelain + + if ($changes) + { + Write-Output 'Please make sure to build the assets properly before pushing, see https://docs.orchardcore.net/en/latest/guides/gulp-pipeline/.' + Write-Output 'The following files changed:' + Write-Output $changes + Write-Output 'You can also download the attached artifact to see the changes.' + Write-Output '' + Write-Output '---------------------------------------' + Write-Output '' + + $changeLines = $changes -split '`n' + $changedFiles = @() + $hasNonCrlfChange = $false + + foreach ($line in $changeLines) + { + if ($line -match '^\s?(M|A|\?\?)\s+(.*)$') + { + $changeType = $matches[1] + $file = $matches[2] + + Write-Output "Diff for: $file" + + if ($changeType -eq 'M') + { + # File is modified; use git diff to get the diff of the modified file. + + # The diff will be sent to stderr so we need to redirect it to stdout to capture it. + git diff -- $file 2>&1 >> tmp.txt + $diffOutput = Get-Content tmp.txt + Remove-Item tmp.txt + + # Filtering out this pattern is necessary because certain CRLF line endings are not replaced by + # gulp-eol, so the output files can still have some CRLF. + if ($($diffOutput ?? '').Contains('CRLF will be replaced by LF the next time Git touches it')) + { + Write-Output "Warning: CRLF will be replaced by LF in $file. Fix this if you can, but certain CRLF line endings can't be replaced." + } + else + { + Write-Output $diffOutput + $hasNonCrlfChange = $true + } + } + elseif ($changeType -eq '??') + { + # File is (untracked); display the file contents. + Get-Content -Path $file + $hasNonCrlfChange = $true + } + + $changedFiles += $file + + Write-Output '' + Write-Output '---------------------------------------' + Write-Output '' + } + } + + if (-not $hasNonCrlfChange) + { + Write-Output 'No non-CRLF changes found. Repository is clean.' + exit 0 + } + + # Convert the array of changed files to a single string with each file on a new line so actions/upload-artifact + # can consume it. + $changedFilesString = $changedFiles -join "`n" + "CHANGED_FILES<> $Env:GITHUB_ENV + + exit -1 + } + else + { + Write-Host "No uncommitted changes found. Repository is clean." + } + - name: Upload changed files as artifact + uses: actions/upload-artifact@v7 + if: failure() + with: + name: changed-files + path: ${{ env.CHANGED_FILES }} + retention-days: 30 + diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 00000000..97c9a8a0 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,27 @@ +name: Backport PR to branch + +on: + issue_comment: + types: [created] + schedule: + # Once a day at 14:00 UTC to clean up old runs. + - cron: '0 14 * * *' + +permissions: + contents: write + issues: write + pull-requests: write + actions: write + +jobs: + backport: + if: ${{ contains(github.event.comment.body, '/backport to') || github.event_name == 'schedule' }} + uses: dotnet/arcade/.github/workflows/backport-base.yml@main + with: + repository_owners: 'CrestApps' + + pr_description_template: | + Backport of #%source_pr_number% to %target_branch% + + /cc %cc_users% + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..be5d9bd5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,64 @@ +name: "CodeQL Security Analysis" + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "25 14 * * 1" + workflow_dispatch: + +permissions: + security-events: write + contents: read + +jobs: + analyze-csharp: + name: Analyze C# + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: csharp + config-file: ./.github/codeql/codeql-config.yml + + - name: Build solution + run: dotnet build CrestApps.Core.slnx -c Release /p:NuGetAudit=false + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:csharp" + + analyze-javascript: + name: Analyze JavaScript + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + config-file: ./.github/codeql/codeql-config.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:javascript-typescript" + diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..6f3840f3 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,27 @@ +# This workflow runs before the Copilot coding agent's firewall is enabled. +# It restores NuGet packages from external sources that would otherwise be blocked. +# See: https://docs.github.com/en/copilot/customizing-copilot/customizing-the-development-environment-for-copilot-coding-agent + +name: Copilot Setup Steps + +on: copilot_setup + +permissions: + contents: read + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 10.0.x + + - name: Restore NuGet packages + run: dotnet restore + diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 00000000..dbac7c84 --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,69 @@ +name: Deploy Documentation + +on: + push: + branches: + - main + tags: + - 'v[0-9]+.[0-9]+.0' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/CrestApps.Core.Docs + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: src/CrestApps.Core.Docs/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Create versioned docs on tag push + if: startsWith(github.ref, 'refs/tags/v') + run: | + FULL_VERSION="${GITHUB_REF#refs/tags/v}" + # Extract major.minor for the doc version (e.g., v3.1.0 -> 3.1.x) + MAJOR="$(echo "$FULL_VERSION" | cut -d. -f1)" + MINOR="$(echo "$FULL_VERSION" | cut -d. -f2)" + DOC_VERSION="${MAJOR}.${MINOR}.x" + echo "Creating docs version ${DOC_VERSION}" + npx docusaurus docs:version "${DOC_VERSION}" + + - name: Build site + run: npm run build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: src/CrestApps.Core.Docs/build + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + diff --git a/.github/workflows/main_ci.yml b/.github/workflows/main_ci.yml new file mode 100644 index 00000000..2e1bcbc8 --- /dev/null +++ b/.github/workflows/main_ci.yml @@ -0,0 +1,44 @@ +name: Main - CI + +on: + # Run it on main and release pushes too, in case we merge from a branch that's not up-to-date with the target branch + # and breaks something after merge (or if we push to main). + push: + paths-ignore: + - '**/*.md' + - 'mkdocs.yml' + - 'src/CrestApps.Core.Docs/**' + branches: [ main, release/** ] + +permissions: + contents: read + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +jobs: + test: + runs-on: ${{ matrix.os }} + name: Build & Test + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "20" + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 10.0.x + - name: Build + # See pr_ci.yml for the reason why we disable NuGet audit warnings. + run: | + dotnet build -c Release -warnaserror /p:TreatWarningsAsErrors=true /p:RunAnalyzers=true /p:NuGetAudit=false + - name: Unit Tests + run: | + dotnet test -c Release --no-build ./tests/CrestApps.Core.Tests/CrestApps.Core.Tests.csproj + diff --git a/.github/workflows/pr_ci.yml b/.github/workflows/pr_ci.yml new file mode 100644 index 00000000..f24525d9 --- /dev/null +++ b/.github/workflows/pr_ci.yml @@ -0,0 +1,40 @@ +name: PR - CI +on: + pull_request: + branches: [ main, release/** ] +permissions: + contents: read +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true +jobs: + build_test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + name: Build & Test + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "20" + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 10.0.x + - name: Build + # We disable NuGet audit warnings, see https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1901-nu1904. + # Security issues being discovered in NuGet packages we use can happen at any time, and thus all our CI builds that + # treat warnings as errors could break anytime, without us changing the code. This prevents that. Treating them as + # We disable NuGet audit warnings here so CI remains stable when advisory data changes independently of this repository. + run: | + dotnet build -c Release -warnaserror /p:TreatWarningsAsErrors=true /p:RunAnalyzers=true /p:NuGetAudit=false + - name: Unit Tests + run: | + dotnet test -c Release --no-build ./tests/CrestApps.Core.Tests/CrestApps.Core.Tests.csproj + diff --git a/.github/workflows/preview_ci.yml b/.github/workflows/preview_ci.yml new file mode 100644 index 00000000..6cb951ae --- /dev/null +++ b/.github/workflows/preview_ci.yml @@ -0,0 +1,53 @@ +name: Preview - CI +on: + workflow_dispatch: + schedule: + # 4:19 AM UTC every day. A random time to avoid peak times of GitHub Actions. + - cron: '19 4 * * *' +permissions: + contents: read + packages: write +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true +jobs: + test: + runs-on: ubuntu-latest + name: Build, Test, Deploy + steps: + - uses: actions/checkout@v6 + - name: Check if should publish + id: check-publish + shell: pwsh + run: | + $hasCommitFromLastDay = ![string]::IsNullOrEmpty((git log --oneline --since '24 hours ago')) + Write-Output "Commits found in the last 24 hours: $hasCommitFromLastDay." + $shouldPublish = ($hasCommitFromLastDay -and '${{ github.event_name }}' -eq 'schedule') -or ('${{ github.event_name }}' -eq 'workflow_dispatch') + "should-publish=$($shouldPublish ? 'true' : 'false')" >> $Env:GITHUB_OUTPUT + - uses: actions/setup-node@v6 + if: steps.check-publish.outputs.should-publish == 'true' + with: + node-version: "20" + - uses: actions/setup-dotnet@v5 + if: steps.check-publish.outputs.should-publish == 'true' + with: + dotnet-version: | + 10.0.x + - name: Set build number + if: steps.check-publish.outputs.should-publish == 'true' + run: echo "BuildNumber=$(( $GITHUB_RUN_NUMBER ))" >> $GITHUB_ENV + - name: Build + if: steps.check-publish.outputs.should-publish == 'true' + # See pr_ci.yml for the reason why we disable NuGet audit warnings. + run: | + dotnet build -c Release -warnaserror /p:TreatWarningsAsErrors=true /p:RunAnalyzers=true /p:NuGetAudit=false + - name: Unit Tests + if: steps.check-publish.outputs.should-publish == 'true' + run: | + dotnet test -c Release --no-build ./tests/CrestApps.Core.Tests/CrestApps.Core.Tests.csproj + - name: Deploy preview NuGet packages + if: steps.check-publish.outputs.should-publish == 'true' + run: | + dotnet pack -c Release --no-build + dotnet nuget push './src/**/*.nupkg' -t 600 -k ${{secrets.CLOUDSMITH_API_KEY}} -n -s https://nuget.cloudsmith.io/crestapps/crestapps-core/v3/index.json --skip-duplicate + diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml new file mode 100644 index 00000000..33cd7900 --- /dev/null +++ b/.github/workflows/release_ci.yml @@ -0,0 +1,57 @@ +name: Release - CI +on: + push: + paths-ignore: + - '**/*.md' + - 'mkdocs.yml' + - 'src/docs/**/*' + tags: + - 'v*.*.*' + +permissions: + contents: read + packages: write +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true +jobs: + test: + runs-on: ${{ matrix.os }} + name: Build, Test, Deploy + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - name: Get the version + id: get_version + run: | + # Strip leading 'v' if present + VERSION="${GITHUB_REF_NAME#v}" + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + shell: bash + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "20" + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 10.0.x + - name: Set build number + if: matrix.os == 'ubuntu-latest' + run: echo "BuildNumber=$(( $GITHUB_RUN_NUMBER ))" >> $GITHUB_ENV + - name: Build + # NuGetAudit is intentionally not disabled here like it is for other CI builds, because we need to address any + # vulnerable packages before releasing a new version. + run: | + dotnet build -c Release -warnaserror /p:TreatWarningsAsErrors=true /p:RunAnalyzers=true -p:Version=${{ steps.get_version.outputs.VERSION }} + - name: Unit Tests + run: | + dotnet test -c Release --no-build ./tests/CrestApps.Core.Tests/CrestApps.Core.Tests.csproj + - name: Deploy release NuGet packages + if: matrix.os == 'ubuntu-latest' + run: | + dotnet pack -c Release --no-build -p:Version=${{ steps.get_version.outputs.VERSION }} -p:TreatWarningsAsErrors=false + dotnet nuget push './src/**/*.nupkg' -t 600 -k ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json --skip-duplicate + diff --git a/.github/workflows/validate_docs.yml b/.github/workflows/validate_docs.yml new file mode 100644 index 00000000..9a6052ec --- /dev/null +++ b/.github/workflows/validate_docs.yml @@ -0,0 +1,41 @@ +name: Validate Documentation + +on: + pull_request: + paths: + - 'src/CrestApps.Core.Docs/**' + +permissions: + contents: read + +jobs: + validate-docs: + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/CrestApps.Core.Docs + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: src/CrestApps.Core.Docs/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build and validate links + run: | + npx docusaurus build 2>&1 | tee build-output.txt + if grep -qi "broken link" build-output.txt; then + echo "" + echo "::error::Broken links detected in documentation!" + grep -i "broken link" build-output.txt + exit 1 + fi + echo "Documentation build successful. All links are valid." + diff --git a/.github/workflows/validate_pr.yml b/.github/workflows/validate_pr.yml new file mode 100644 index 00000000..7bc185a4 --- /dev/null +++ b/.github/workflows/validate_pr.yml @@ -0,0 +1,27 @@ +name: Validate Pull Request + +on: + push: + pull_request_target: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + +jobs: + validate-pull-request: + name: Validate Pull Request + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - name: Check for Merge Conflict in PR + # v3.0.0 + uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + commentOnDirty: "This pull request has merge conflicts. Please resolve those before requesting a review." + dirtyLabel: "merge conflict" + # The default 120 is too long. The mergeable state is usually calculated by GitHub within seconds. + retryAfter: 5 + diff --git a/.github/workflows/validate_prompts.yml b/.github/workflows/validate_prompts.yml new file mode 100644 index 00000000..705d6531 --- /dev/null +++ b/.github/workflows/validate_prompts.yml @@ -0,0 +1,210 @@ +name: Validate AI Templates +on: + pull_request: + branches: [ main, release/** ] + paths: + - '**/Templates/**/*.md' +permissions: + contents: read +concurrency: + group: validate-ai-templates-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true +jobs: + validate_templates: + runs-on: ubuntu-latest + name: Validate AI Templates + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 10.0.x + - name: Build prompt validation tool + run: | + dotnet build -c Release src/Common/CrestApps.Core.Templates/CrestApps.Core.Templates.csproj /p:NuGetAudit=false + - name: Validate template file structure + shell: bash + run: | + EXIT_CODE=0 + shopt -s globstar nullglob + + for file in src/**/Templates/**/*.md; do + [ -f "$file" ] || continue + + # Check for valid front matter structure + if head -1 "$file" | grep -q "^---"; then + # Has front matter; verify it has a closing delimiter + CLOSE_COUNT=$(grep -c "^---" "$file" || true) + if [ "$CLOSE_COUNT" -lt 2 ]; then + echo "::error file=$file::Front matter is missing closing '---' delimiter" + EXIT_CODE=1 + fi + fi + + # Check that file is not empty + if [ ! -s "$file" ]; then + echo "::error file=$file::Template file is empty" + EXIT_CODE=1 + fi + + echo "✓ $file" + done + + if [ $EXIT_CODE -ne 0 ]; then + echo "" + echo "::error::One or more template files have structural issues" + fi + + exit $EXIT_CODE + - name: Validate YAML front matter + shell: python3 {0} + run: | + import glob, re, sys, yaml + + exit_code = 0 + + for path in sorted(glob.glob('src/**/Templates/**/*.md', recursive=True)): + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + + # Only check files with front matter + stripped_content = content.lstrip() + if not stripped_content.startswith('---'): + continue + + # Extract front matter between --- delimiters + parts = stripped_content.split('---', 2) + if len(parts) < 3: + continue + + front_matter = parts[1] + + # Check for tab characters (YAML forbids tabs for indentation) + for i, line in enumerate(front_matter.split('\n'), start=2): + if '\t' in line: + print(f"::error file={path},line={i}::YAML front matter contains " + f"tab character(s). YAML requires spaces for indentation.") + exit_code = 1 + break + + # Validate YAML syntax + try: + yaml.safe_load(front_matter) + except yaml.YAMLError as e: + line_num = 1 + if hasattr(e, 'problem_mark') and e.problem_mark: + line_num = e.problem_mark.line + 2 # +2 for 0-based and --- line + print(f"::error file={path},line={line_num}::Invalid YAML in front matter: {e}") + exit_code = 1 + + if exit_code == 0: + print("✓ All YAML front matter is valid") + + sys.exit(exit_code) + - name: Validate JSON in fenced code blocks + shell: python3 {0} + run: | + import glob, json, re, sys + + # Pattern to extract fenced ```json blocks + fence_pattern = re.compile(r'```json\s*\n(.*?)```', re.DOTALL) + # Patterns that indicate an intentional schema description, not literal JSON + schema_indicators = re.compile(r'\btrue\s*\|\s*false\b|<[a-zA-Z_]+>') + + exit_code = 0 + + for path in sorted(glob.glob('src/**/Templates/**/*.md', recursive=True)): + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + + for i, match in enumerate(fence_pattern.finditer(content), 1): + json_text = match.group(1).strip() + if not json_text: + continue + + try: + json.loads(json_text) + except json.JSONDecodeError: + # If it contains schema description patterns (e.g., "true | false", + # ""), it is intentionally not literal JSON. + if schema_indicators.search(json_text): + continue + + line_num = content[:match.start()].count('\n') + 1 + print(f"::error file={path},line={line_num}::Fenced ```json block #{i} " + f"contains invalid JSON. If this is a schema description, use " + f"placeholders like or true | false.") + exit_code = 1 + + if exit_code == 0: + print("✓ All fenced JSON blocks are valid") + + sys.exit(exit_code) + - name: Validate Parameters front matter syntax + shell: python3 {0} + run: | + import glob, re, sys + + exit_code = 0 + + for path in sorted(glob.glob('src/**/Templates/**/*.md', recursive=True)): + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + + # Only check files with front matter + if not content.lstrip().startswith('---'): + continue + + # Extract front matter between --- delimiters + parts = content.lstrip().split('---', 2) + if len(parts) < 3: + continue + + front_matter = parts[1] + lines = front_matter.split('\n') + + in_parameters = False + param_line_num = 0 + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped: + continue + + # Detect Parameters: key + if stripped.startswith('Parameters:'): + in_parameters = True + param_line_num = i + 2 # +2 for 1-based and the --- line + continue + + # Lines within Parameters block start with whitespace + if in_parameters and (line.startswith('\t') or line.startswith(' ')): + entry = stripped + # Strip bullet marker + if entry.startswith('- '): + entry = entry[2:] + + # Each parameter entry must have name: description format + if ':' not in entry: + actual_line = i + 2 + print(f"::error file={path},line={actual_line}::Parameter entry " + f"'{stripped}' is missing ':' separator. Expected format: " + f"'- name: description'") + exit_code = 1 + elif entry.index(':') == 0: + actual_line = i + 2 + print(f"::error file={path},line={actual_line}::Parameter entry " + f"'{stripped}' has empty name. Expected format: " + f"'- name: description'") + exit_code = 1 + else: + in_parameters = False + + if exit_code == 0: + print("✓ All Parameters entries are valid") + + sys.exit(exit_code) + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8ee5731f --- /dev/null +++ b/.gitignore @@ -0,0 +1,473 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +is-cache/ +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + + +## +## Custom +## +lib +src/DealerSolutions.FormatterTest +**/App_Data* +!**/App_Data/HostStorage +ms-cache + +## +## Docusaurus (src/docs) +## +src/docs/node_modules/ +src/docs/build/ +src/docs/.docusaurus/ +src/docs/.cache-loader/ \ No newline at end of file diff --git a/CrestApps.Core.slnx b/CrestApps.Core.slnx new file mode 100644 index 00000000..8dca0e4a --- /dev/null +++ b/CrestApps.Core.slnx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CrestAppsLogo.png b/CrestAppsLogo.png new file mode 100644 index 00000000..15fb05c1 Binary files /dev/null and b/CrestAppsLogo.png differ diff --git a/CrestAppsLogo.svg b/CrestAppsLogo.svg new file mode 100644 index 00000000..85534ce0 --- /dev/null +++ b/CrestAppsLogo.svg @@ -0,0 +1,86 @@ + + CrestApps Package-ai + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..767ac459 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,118 @@ + + + + + + + + net10.0 + + + + net10.0 + + + + + $(DefaultTargetFramework) + + + + + $(CommonTargetFrameworks) + enable + Mike Alhayek + CrestApps + CrestAppsLogo.png + README.md + true + MIT + https://github.com/CrestApps/CrestApps.Core + https://crestapps.com + CrestApps.Core provides the shared, framework-agnostic CrestApps libraries for AI, orchestration, chat, templating, document processing, storage, and MVC sample applications built on ASP.NET Core. + CrestApps Core AI ASP.NET + + + + + True + CrestAppsLogo.png + + + True + README.md + + + + + 1.0.0 + preview + $(VersionSuffix)-$(BuildNumber) + + + + + + true + + + + + latest-Recommended + + + $(NoWarn);CA1805 + + + $(NoWarn);CA1304;CA1305;CA1310 + + + $(NoWarn);CA1311 + + + $(NoWarn);CA1000 + + + $(NoWarn);CA1848 + + + $(NoWarn);CA1720 + + + $(NoWarn);CA1051 + + + $(NoWarn);CA1200 + + + $(NoWarn);CA1711 + + + $(NoWarn);CA1725 + + + $(NoWarn);CA1716 + + + $(NoWarn);CA1001 + + + $(NoWarn);CA2201 + + + $(NoWarn);CA1707 + + + $(NoWarn);CA1727 + + + $(NoWarn);CA1861 + + + $(NoWarn);NU1605 + + $(NoWarn),1573,1591,1712 + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..f1493951 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,79 @@ + + + true + true + 1.1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 00000000..93395c88 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 2e2cbd7e..46f226e7 100644 --- a/README.md +++ b/README.md @@ -1 +1,93 @@ -"# CrestApps.Core" +# CrestApps.Core + +**CrestApps.Core is the AI application framework for .NET.** It gives you the orchestration, management, chat, document, RAG, MCP, A2A, agent, reporting, and extensibility building blocks needed to add production-ready AI to an existing application without stitching together a pile of disconnected SDK samples. + +## Why it exists + +Most teams start AI integration with a provider SDK, then quickly run into the real complexity: + +- Model and provider switching +- Prompt and profile reuse +- Chat session management +- Document and knowledge retrieval +- Tool calling and custom functions +- Reporting, consumption tracking, and lead workflows +- Live-agent handoff and post-session automation +- Protocol integration like MCP and A2A +- Orcestrator integration like copilot orchestrator. + +`CrestApps.Core` packages that complexity into reusable .NET services so you can move faster, keep control of behavior, and ship AI features with less custom plumbing. + +## What you get + +- **AI management** for profiles, connections, deployments, data sources, templates, MCP resources, prompts, and external hosts +- **Reusable AI profiles** so every session can start with predefined behavior, settings, tools, prompts, and retrieval rules +- **Chat interactions** for provider-agnostic playground and production chat experiences +- **Document upload and processing** for summarization, Q&A, extraction, tabulation, and knowledge workflows +- **RAG support** across attached documents, search indexes, and user memory, including configurable preemptive RAG +- **AI agents** with agent-to-agent handoff through A2A +- **MCP server and MCP client capabilities** including resource, prompt, and external host management +- **Custom AI functions** for application-specific actions and tool calling +- **Copilot orchestrator integration** +- **Template-driven prompts and profile definitions** for cleaner code and reusable system behavior +- **Chat widgets and business workflows** including chat metrics, data extraction, goal conversion, post-session processing, and live-agent handoff +- **Usage and lead reporting** for AI consumption and customer engagement scenarios +- **AI memory** for more personal, user-aware experiences +- **Full customization** from code-level extension points to default runtime behavior + +## Common use cases + +- **Lead generation chat** on a marketing site that qualifies visitors, captures contact details, and routes hot leads by email or workflow +- **Knowledge-base assistants** restricted to approved business content stored in documents, Elasticsearch, or Azure AI Search +- **Support automation** that starts with AI and escalates to a live agent on a third-party platform when needed +- **Specialized agent teams** where multiple AI agents handle different tasks and coordinate through A2A +- **Document analysis workbenches** for reports, contracts, internal documentation, or uploaded files +- **Reusable internal copilots** for operations, sales, research, and support teams +- **Custom workflow automation** driven by AI tool calls into your own services and APIs + +See the full use-case guide at **[core.crestapps.com](https://core.crestapps.com)**. + +## Quick start + +```powershell +git clone https://github.com/CrestApps/CrestApps.Core.git +cd CrestApps.Core +dotnet build .\CrestApps.Core.slnx -c Release /p:NuGetAudit=false +dotnet test .\tests\CrestApps.Core.Tests\CrestApps.Core.Tests.csproj -c Release /p:NuGetAudit=false +dotnet run --project .\src\Startup\CrestApps.Core.Mvc.Web\CrestApps.Core.Mvc.Web.csproj +``` + +## Learn more + +- **Documentation:** +- **Issues:** +- **Preview feed:** +- **Preview source URL:** `https://nuget.cloudsmith.io/crestapps/crestapps-core/v3/index.json` + +## Repository layout + +```text +src/ +├── Abstractions/ +├── Primitives/ +├── Resources/ +├── Stores/ +├── Utilities/ +├── Startup/ +│ ├── CrestApps.Core.Aspire.AppHost/ +│ ├── CrestApps.Core.Mvc.Web/ +│ ├── CrestApps.Core.Mvc.Samples.A2AClient/ +│ └── CrestApps.Core.Mvc.Samples.McpClient/ +└── CrestApps.Core.Docs/ + +tests/ +└── CrestApps.Core.Tests/ +``` + +## Community + +This project is actively evolving to help the .NET community adopt AI with strong architecture, extensibility, security-minded defaults, and support for modern protocols. If it helps you, please open issues, share feedback, report bugs, star the repository, or contribute code. + +## License + +MIT diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..84af11b7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +We primarily support the latest version only. For serious security issues, we'll patch several older versions too. + +## Reporting a Vulnerability + +Please report security issues privately, via email, to [mike@crestapps.com](mailto:mike@crestapps.com). diff --git a/copi.cmd b/copi.cmd new file mode 100644 index 00000000..8fd6662a --- /dev/null +++ b/copi.cmd @@ -0,0 +1,6 @@ +@echo off +REM Set instructions folder +set COPILOT_CUSTOM_INSTRUCTIONS_DIRS=%~dp0copilot-cli + +REM Run Copilot CLI with any arguments passed +copilot --allow-all %* diff --git a/copilot-cli/.github/instructions/commiting.instructions.md b/copilot-cli/.github/instructions/commiting.instructions.md new file mode 100644 index 00000000..1d805895 --- /dev/null +++ b/copilot-cli/.github/instructions/commiting.instructions.md @@ -0,0 +1,54 @@ +# CrestApps.Core Copilot CLI Instructions + +Use these instructions when working on `CrestApps.Core` with Copilot CLI. + +## Project overview + +`CrestApps.Core` is the standalone framework repository for CrestApps shared .NET libraries. It contains the reusable AI, orchestration, chat, storage, templating, SignalR, and protocol packages plus sample hosts and the docs site. + +- **Target framework:** .NET 10 +- **Docs project:** `src\CrestApps.Core.Docs` +- **Tests:** `tests\CrestApps.Core.Tests` +- **Sample hosts:** `src\Startup\CrestApps.Core.Mvc.Web`, `src\Startup\CrestApps.Core.Aspire.AppHost` + +For Orchard Core-specific implementation guidance, use . + +## Build commands + +```bash +dotnet build .\CrestApps.Core.slnx -c Release /p:NuGetAudit=false +dotnet test .\tests\CrestApps.Core.Tests\CrestApps.Core.Tests.csproj -c Release /p:NuGetAudit=false +``` + +Root asset tooling: + +```bash +npm install +npm run rebuild +``` + +Docs site: + +```bash +cd src\CrestApps.Core.Docs +npm install +npm run build +``` + +## Working rules + +- Keep changes focused on `CrestApps.Core` +- Update `src\CrestApps.Core.Docs` when public behavior or setup changes +- Add or update changelog entries for release-worthy changes +- Prefer repository tools and existing patterns over one-off scripts +- Do not describe Orchard Core module behavior as if it were part of this repository + +## Common paths + +- `src\Abstractions` +- `src\Primitives` +- `src\Stores` +- `src\Utilities` +- `src\Startup` +- `src\CrestApps.Core.Docs` +- `tests\CrestApps.Core.Tests` diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..df316f59 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +core.crestapps.com diff --git a/global.json b/global.json new file mode 100644 index 00000000..02d83b30 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestMajor" + } +} diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..f87d594b --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,366 @@ +var fs = require("graceful-fs"), + glob = require("glob"), + path = require("path-posix"), + gulp = require("gulp"), + gulpif = require("gulp-if"), + newer = require("gulp-newer"), + plumber = require("gulp-plumber"), + sourcemaps = require("gulp-sourcemaps"), + less = require("gulp-less"), + scss = require("gulp-dart-sass"), + minify = require("gulp-minifier"), + typescript = require("gulp-typescript"), + terser = require("gulp-terser"), + rename = require("gulp-rename"), + concat = require("gulp-concat"), + header = require("gulp-header"), + eol = require("gulp-eol"), + log = require('fancy-log'), + postcss = require('gulp-postcss'), + rtl = require('postcss-rtl'), + babel = require('gulp-babel'); +const { finished } = require("stream/promises"); + +// For compat with older versions of Node.js. +require("es6-promise").polyfill(); + +// Suppress Node.js deprecation warnings during build output. +// process.noDeprecation = true; + +// To suppress memory leak warning from gulp.watch(). +require("events").EventEmitter.prototype._maxListeners = 100; + +/* +** GULP TASKS +*/ + +// Incremental build (each asset group is built only if one or more inputs are newer than the output). +gulp.task("build-assets", function () { + var assetGroupTasks = getAssetGroups().map(function (assetGroup) { + var doRebuild = false; + return createAssetGroupTask(assetGroup, doRebuild); + }); + return Promise.all(assetGroupTasks.map(waitForTaskCompletion)); +}); + +// Full rebuild (all assets groups are built regardless of timestamps). +gulp.task("rebuild-assets", function () { + var assetGroupTasks = getAssetGroups().map(function (assetGroup) { + var doRebuild = true; + return createAssetGroupTask(assetGroup, doRebuild); + }); + return Promise.all(assetGroupTasks.map(waitForTaskCompletion)); +}); + +// Continuous watch (each asset group is built whenever one of its inputs changes). +gulp.task("watch", function () { + getAssetGroups().forEach(function (assetGroup) { + var watchPaths = assetGroup.inputPaths.concat(assetGroup.watchPaths); + var inputWatcher; + function createWatcher() { + inputWatcher = gulp.watch(watchPaths); + inputWatcher.on('change', function (watchedPath) { + var isConcat = path.basename(assetGroup.outputFileName, path.extname(assetGroup.outputFileName)) !== "@"; + if (isConcat) + console.log("Asset file '" + watchedPath + "' was changed, rebuilding asset group with output '" + assetGroup.outputPath + "'."); + else + console.log("Asset file '" + watchedPath + "' was changed, rebuilding asset group."); + var doRebuild = true; + createAssetGroupTask(assetGroup, doRebuild); + }); + } + + createWatcher(); + + gulp.watch(assetGroup.manifestPath).on('change', function (watchedPath) { + console.log("Asset manifest file '" + watchedPath + "' was changed, restarting watcher."); + inputWatcher.close(); + createWatcher(); + }); + }); +}); + +gulp.task('help', function () { + log(` + Usage: gulp [TASK] + Tasks: + build Incremental build (each asset group is built only if one or more inputs are newer than the output). + rebuild Full rebuild (all assets groups are built regardless of timestamps). + watch Continuous watch (each asset group is built whenever one of its inputs changes). + `); +}); + +gulp.task('build', gulp.series(['build-assets'])); +gulp.task('rebuild', gulp.series(['rebuild-assets'])); +gulp.task('default', gulp.series(['build'])); + +/* +** ASSET GROUPS +*/ + +function getAssetGroups() { + var assetManifestPaths = glob.sync("./src/{Primitives,Resources}/*/Assets.json", {}); + var assetGroups = []; + assetManifestPaths.forEach(function (assetManifestPath) { + var assetManifest = require("./" + assetManifestPath); + assetManifest.forEach(function (assetGroup) { + resolveAssetGroupPaths(assetGroup, assetManifestPath); + assetGroups.push(assetGroup); + }); + }); + return assetGroups; +} + +function resolveAssetGroupPaths(assetGroup, assetManifestPath) { + assetGroup.manifestPath = assetManifestPath.replace(/\\/g, '/'); + assetGroup.basePath = path.dirname(assetGroup.manifestPath); + + var inputPaths = []; + + // The inputPaths can contain either a physical path to a file or a path with a wildcard. + // It's crucial to maintain the order of each file based on its position in the assets.json file. + // When a path contains a wildcard, we need to convert the wildcard to physical paths + // and sort them independently of the previous paths to ensure consistent concatenation. + assetGroup.inputs.forEach(inputPath => { + + var resolvedPath = path.resolve(path.join(assetGroup.basePath, inputPath)).replace(/\\/g, '/'); + var isNodeModulesPath = inputPath.startsWith("node_modules/") || inputPath.startsWith("./node_modules/"); + + if (resolvedPath.includes('*')) { + var sortedPaths = glob.sync(resolvedPath, {}); + + // Fall back to repo root 'node_modules' when modules are not restored locally. + if (isNodeModulesPath && sortedPaths.length === 0) { + var rootResolvedPath = path.resolve(inputPath).replace(/\\/g, '/'); + sortedPaths = glob.sync(rootResolvedPath, {}); + } + + sortedPaths.sort(); + + sortedPaths.forEach(sortedPath => { + inputPaths.push(sortedPath.replace(/\\/g, '/')); + }); + } else { + // Fall back to repo root 'node_modules' when modules are not restored locally. + if (isNodeModulesPath && !fs.existsSync(resolvedPath)) { + var rootResolvedPath = path.resolve(inputPath).replace(/\\/g, '/'); + if (fs.existsSync(rootResolvedPath)) { + resolvedPath = rootResolvedPath; + } + } + + inputPaths.push(resolvedPath); + } + }); + + assetGroup.inputPaths = inputPaths; + + assetGroup.watchPaths = []; + if (!!assetGroup.watch) { + assetGroup.watchPaths = assetGroup.watch.map(function (watchPath) { + return path.resolve(path.join(assetGroup.basePath, watchPath)).replace(/\\/g, '/'); + }); + } + assetGroup.outputPath = path.resolve(path.join(assetGroup.basePath, assetGroup.output)).replace(/\\/g, '/'); + assetGroup.outputDir = path.dirname(assetGroup.outputPath); + assetGroup.outputFileName = path.basename(assetGroup.output); + // Uncomment to copy assets to wwwroot + //assetGroup.webroot = path.join("./src/OrchardCore.Cms.Web/wwwroot/", path.basename(assetGroup.basePath), path.dirname(assetGroup.output)); +} + +function waitForTaskCompletion(task) { + if (!task) { + return Promise.resolve(); + } + + return typeof task.then === "function" ? task : finished(task); +} + +function createAssetGroupTask(assetGroup, doRebuild) { + var outputExt = path.extname(assetGroup.output).toLowerCase(); + var doConcat = path.basename(assetGroup.outputFileName, outputExt) !== "@"; + if (doConcat && !doRebuild) { + // Force a rebuild of this asset group is the asset manifest file itself is newer than the output. + var assetManifestStats = fs.statSync(assetGroup.manifestPath); + var outputStats = fs.existsSync(assetGroup.outputPath) ? fs.statSync(assetGroup.outputPath) : null; + doRebuild = !outputStats || assetManifestStats.mtime > outputStats.mtime; + } + + if (assetGroup.copy === true) { + return buildCopyPipeline(assetGroup, doRebuild); + } + else { + switch (outputExt) { + case ".css": + return buildCssPipeline(assetGroup, doConcat, doRebuild); + case ".js": + return buildJsPipeline(assetGroup, doConcat, doRebuild); + } + } +} + +/* +** PROCESSING PIPELINES +*/ + +function buildCssPipeline(assetGroup, doConcat, doRebuild) { + assetGroup.inputPaths.forEach(function (inputPath) { + var ext = path.extname(inputPath).toLowerCase(); + if (ext !== ".scss" && ext !== ".less" && ext !== ".css") + throw "Input file '" + inputPath + "' is not of a valid type for output file '" + assetGroup.outputPath + "'."; + }); + var generateSourceMaps = assetGroup.hasOwnProperty("generateSourceMaps") ? assetGroup.generateSourceMaps : false; + var generateRTL = assetGroup.hasOwnProperty("generateRTL") ? assetGroup.generateRTL : false; + var containsLessOrScss = assetGroup.inputPaths.some(function (inputPath) { + var ext = path.extname(inputPath).toLowerCase(); + return ext === ".less" || ext === ".scss"; + }); + // Source maps are useless if neither concatenating nor transforming. + if ((!doConcat || assetGroup.inputPaths.length < 2) && !containsLessOrScss) + generateSourceMaps = false; + + var minifiedStream = gulp.src(assetGroup.inputPaths) // Minified output, source mapping completely disabled. + .pipe(gulpif(!doRebuild, + gulpif(doConcat, + newer(assetGroup.outputPath), + newer({ + dest: assetGroup.outputDir, + ext: ".css" + })))) + .pipe(plumber()) + .pipe(gulpif("*.less", less())) + .pipe(gulpif("*.scss", scss({ + precision: 10, + silenceDeprecations: ["legacy-js-api"], + + }))) + .pipe(gulpif(doConcat, concat(assetGroup.outputFileName))) + .pipe(gulpif(generateRTL, postcss([rtl()]))) + .pipe(minify({ + minify: true, + minifyHTML: { + collapseWhitespace: true, + conservativeCollapse: true, + }, + minifyJS: { + sourceMap: true + }, + minifyCSS: true + })) + .pipe(rename({ + suffix: ".min" + })) + .pipe(eol('\n')) + .pipe(gulp.dest(assetGroup.outputDir)); + // Uncomment to copy assets to wwwroot + //.pipe(gulp.dest(assetGroup.webroot)); + var devStream = gulp.src(assetGroup.inputPaths) // Non-minified output, with source mapping + .pipe(gulpif(!doRebuild, + gulpif(doConcat, + newer(assetGroup.outputPath), + newer({ + dest: assetGroup.outputDir, + ext: ".css" + })))) + .pipe(plumber()) + .pipe(gulpif(generateSourceMaps, sourcemaps.init())) + .pipe(gulpif("*.less", less())) + .pipe(gulpif("*.scss", scss({ + precision: 10, + silenceDeprecations: ["legacy-js-api"] + }))) + .pipe(gulpif(doConcat, concat(assetGroup.outputFileName))) + .pipe(gulpif(generateRTL, postcss([rtl()]))) + .pipe(gulpif(generateSourceMaps, sourcemaps.write())) + .pipe(eol('\n')) + .pipe(gulp.dest(assetGroup.outputDir)); + // Uncomment to copy assets to wwwroot + //.pipe(gulp.dest(assetGroup.webroot)); + return Promise.all([finished(minifiedStream), finished(devStream)]); +} + +function buildJsPipeline(assetGroup, doConcat, doRebuild) { + assetGroup.inputPaths.forEach(function (inputPath) { + var ext = path.extname(inputPath).toLowerCase(); + if (ext !== ".ts" && ext !== ".js") + throw "Input file '" + inputPath + "' is not of a valid type for output file '" + assetGroup.outputPath + "'."; + }); + var generateSourceMaps = assetGroup.hasOwnProperty("generateSourceMaps") ? assetGroup.generateSourceMaps : false; + // Source maps are useless if neither concatenating nor transforming. + if ((!doConcat || assetGroup.inputPaths.length < 2) && !assetGroup.inputPaths.some(function (inputPath) { return path.extname(inputPath).toLowerCase() === ".ts"; })) + generateSourceMaps = false; + + var tsCompilerOptions = assetGroup.hasOwnProperty("tsCompilerOptions") ? assetGroup.tsCompilerOptions : { + declaration: false, + noImplicitAny: true, + noEmitOnError: true, + target: "es5", + }; + + function createJsStream(enableSourceMaps) { + return gulp.src(assetGroup.inputPaths) + .pipe(gulpif(!doRebuild, + gulpif(doConcat, + newer(assetGroup.outputPath), + newer({ + dest: assetGroup.outputDir, + ext: ".js" + })))) + .pipe(plumber()) + .pipe(gulpif(enableSourceMaps, sourcemaps.init())) + .pipe(gulpif("*.ts", typescript(tsCompilerOptions))) + .pipe(babel({ + "presets": [ + [ + "@babel/preset-env", + { + "modules": false + }, + "@babel/preset-flow" + ] + ] + })) + .pipe(gulpif(doConcat, concat(assetGroup.outputFileName))) + .pipe(header( + "/*\n" + + "** NOTE: This file is generated by Gulp and should not be edited directly!\n" + + "** Any changes made directly to this file will be overwritten next time its asset group is processed by Gulp.\n" + + "*/\n\n")); + } + + var devStream = createJsStream(generateSourceMaps) + .pipe(gulpif(generateSourceMaps, sourcemaps.write())) + .pipe(gulp.dest(assetGroup.outputDir)); + // Uncomment to copy assets to wwwroot + //.pipe(gulp.dest(assetGroup.webroot)); + + var minifiedStream = createJsStream(false) + .pipe(terser()) + .pipe(rename({ + suffix: ".min" + })) + .pipe(eol('\n')) + .pipe(gulp.dest(assetGroup.outputDir)); + // Uncomment to copy assets to wwwroot + //.pipe(gulp.dest(assetGroup.webroot)); + + return Promise.all([finished(devStream), finished(minifiedStream)]); +} + +function buildCopyPipeline(assetGroup, doRebuild) { + var stream = gulp.src(assetGroup.inputPaths); + + if (!doRebuild) { + stream = stream.pipe(newer(assetGroup.outputDir)) + } + + var renameFile = assetGroup.outputFileName != "@"; + + stream = stream + .pipe(gulpif(renameFile, rename(assetGroup.outputFileName))) + .pipe(gulp.dest(assetGroup.outputDir)); + // Uncomment to copy assets to wwwroot + //.pipe(gulp.dest(assetGroup.webroot)); + + return stream; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..d17595e2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7876 @@ +{ + "name": "crestapps.core", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crestapps.core", + "dependencies": { + "chart.js": "^4.5.1", + "extendable-media-recorder": "^9.2.36" + }, + "devDependencies": { + "@babel/core": "^7.22.11", + "@babel/preset-env": "^7.22.10", + "@babel/preset-flow": "7.27.1", + "child_process": "^1.0.2", + "es6-promise": "4.2.8", + "fancy-log": "^2.0.0", + "glob": "^13.0.6", + "graceful-fs": "4.2.11", + "gulp": "^5.0.1", + "gulp-babel": "8.0.0", + "gulp-concat": "2.6.1", + "gulp-dart-sass": "1.1.0", + "gulp-eol": "0.2.0", + "gulp-header": "^2.0.12", + "gulp-if": "3.0.0", + "gulp-less": "5.0.0", + "gulp-minifier": "^3.5.0", + "gulp-newer": "1.4.0", + "gulp-plumber": "1.2.1", + "gulp-postcss": "^10.0.0", + "gulp-rename": "2.1.0", + "gulp-sourcemaps": "^3.0.0", + "gulp-terser": "2.1.0", + "gulp-typescript": "^5.0.1", + "merge-stream": "2.0.0", + "path-posix": "1.0.0", + "postcss": "8.5.6", + "postcss-rtl": "^2.0.0", + "rtlcss": "4.3.0", + "source-map": "^0.7.4", + "typescript": "^5.4.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "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/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@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-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "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.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "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-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "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-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "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-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "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-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "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-transform-flow-strip-types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "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-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "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-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "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-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "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-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "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-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "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-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "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-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "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-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "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-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "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-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "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-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", + "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.27.1.tgz", + "integrity": "sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-flow-strip-types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", + "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/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/@gulp-sourcemaps/identity-map/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "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.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async-done": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/async-settle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-done": "^2.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/automation-events": { + "version": "7.1.15", + "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-7.1.15.tgz", + "integrity": "sha512-NsHJlve3twcgs8IyP4iEYph7Fzpnh6klN7G5LahwvypakBjFbsiGHJxrqTmeHKREdu/Tx6oZboqNI0tD4MnFlA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", + "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.6", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.6" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "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==", + "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/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/broker-factory": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.13.tgz", + "integrity": "sha512-H2VALe31mEtO/SRcNp4cUU5BAm1biwhc/JaF77AigUuni/1YT0FLCJfbUxwIEs9y6Kssjk2fmXgf+Y9ALvmKlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-unique-numbers": "^9.0.26", + "tslib": "^2.8.1", + "worker-factory": "^7.0.48" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "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==", + "dev": true, + "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/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/child_process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", + "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==", + "dev": true, + "license": "ISC" + }, + "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/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/clean-css/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/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/cloneable-readable/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cloneable-readable/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "license": "ISC", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/concat-with-sourcemaps/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/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/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css/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/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" + } + }, + "node_modules/debug-fabulous/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties/node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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==", + "dev": true, + "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/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/each-props": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extendable-media-recorder": { + "version": "9.2.36", + "resolved": "https://registry.npmjs.org/extendable-media-recorder/-/extendable-media-recorder-9.2.36.tgz", + "integrity": "sha512-xp8RC74Iy4V/CF/xUfIbiiLJUwj7JlCqorE5k/7cSjjUMV5TjErZ4l0IAfPQD9+1t3Yq2wPb0GgLcoOPdaDi0g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "media-encoder-host": "^9.0.27", + "multi-buffer-data-view": "^6.0.26", + "recorder-audio-worklet": "^6.0.57", + "standardized-audio-context": "^25.3.77", + "subscribable-things": "^2.1.57", + "tslib": "^2.8.1" + } + }, + "node_modules/extendable-media-recorder-wav-encoder-broker": { + "version": "7.0.125", + "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-broker/-/extendable-media-recorder-wav-encoder-broker-7.0.125.tgz", + "integrity": "sha512-HVmznJvyG+eFZJRYLd9h3OF0oNNIGEmEHAP4IQ0Y5gwxJcmrFhVmGB4hLi1GT/jNM8aSoCxIePVkCX+5tuGvpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "broker-factory": "^3.1.13", + "extendable-media-recorder-wav-encoder-worker": "^8.0.121", + "tslib": "^2.8.1" + } + }, + "node_modules/extendable-media-recorder-wav-encoder-worker": { + "version": "8.0.121", + "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-worker/-/extendable-media-recorder-wav-encoder-worker-8.0.121.tgz", + "integrity": "sha512-UBBgWkyE9fpCLDdrWdTZM56FkImAljpUuxr6+y9W6LHvY7XWhkZP+yO5uZUUquS5IpsBlY2uKOWpKwiLdo3FOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1", + "worker-factory": "^7.0.48" + } + }, + "node_modules/fancy-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-2.0.0.tgz", + "integrity": "sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-support": "^1.1.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.26", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.26.tgz", + "integrity": "sha512-3Mtq8p1zQinjGyWfKeuBunbuFoixG72AUkk4VvzbX4ykCW9Q4FzRaNyIlfQhUjnKw2ARVP+/CKnoyr6wfHftig==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/fined": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flagged-respawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/flush-write-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/flush-write-stream/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/flush-write-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fork-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", + "integrity": "sha512-Pqq5NnT78ehvUnAk/We/Jr22vSvanRlFTpAmQ88xBY/M1TlHe+P0ILuEyXS595ysdGfaj22634LBkGMA2GTcpA==", + "dev": true, + "license": "BSD" + }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob-stream": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.3.tgz", + "integrity": "sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-stream/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-watcher": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-done": "^2.0.0", + "chokidar": "^3.5.3" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glogg": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", + "dev": true, + "license": "MIT", + "dependencies": { + "sparkles": "^2.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "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/gulp": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.1.tgz", + "integrity": "sha512-PErok3DZSA5WGMd6XXV3IRNO0mlB+wW3OzhFJLEec1jSERg2j1bxJ6e5Fh6N6fn3FH2T9AP4UYNb/pYlADB9sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.1.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.2" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-babel": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gulp-babel/-/gulp-babel-8.0.0.tgz", + "integrity": "sha512-oomaIqDXxFkg7lbpBou/gnUkX51/Y/M2ZfSjL2hdqXTAlSWZcgZtd2o0cOH0r/eE8LWD0+Q/PsLsr2DKOoqToQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "plugin-error": "^1.0.1", + "replace-ext": "^1.0.0", + "through2": "^2.0.0", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/gulp-cli": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.1.0.tgz", + "integrity": "sha512-zZzwlmEsTfXcxRKiCHsdyjZZnFvXWM4v1NqBJSYbuApkvVKivjcmOS2qruAJ+PkEHLFavcDKH40DPc1+t12a9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.1", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-concat": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", + "integrity": "sha512-a2scActrQrDBpBbR3WUZGyGS1JEPLg5PZJdIa7/Bi3GuKAmPYDK6SFhy/NZq5R8KsKKFvtfR0fakbUCcKGCCjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "concat-with-sourcemaps": "^1.0.0", + "through2": "^2.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-dart-sass": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-dart-sass/-/gulp-dart-sass-1.1.0.tgz", + "integrity": "sha512-wc04rAk3ycBk4Z+vTVh5tPxgKNjtlfjqC7BiVG4ZvU8JswdzZs17Hn141RYTu+e7J8FivbL3VOhL5+Z+wvU0Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.3.0", + "lodash.clonedeep": "^4.3.2", + "plugin-error": "^1.0.1", + "replace-ext": "^1.0.0", + "sass": "^1.49.0", + "strip-ansi": "^4.0.0", + "through2": "^2.0.0", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gulp-dart-sass/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gulp-dart-sass/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gulp-dart-sass/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/gulp-dart-sass/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-dart-sass/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/gulp-dart-sass/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/gulp-eol": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/gulp-eol/-/gulp-eol-0.2.0.tgz", + "integrity": "sha512-YaUQld+7A3h1iOHs7XTdEboYUg8GjA+3DmWtgvZ9sb1r0tDpB27cTtgaoD83oTm9Mp2Hmwge6AQMqPxwUjQ/xA==", + "dev": true, + "dependencies": { + "plugin-error": "~1.0", + "through2": "~0.4" + } + }, + "node_modules/gulp-eol/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-eol/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/gulp-eol/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-eol/node_modules/through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "node_modules/gulp-eol/node_modules/xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==", + "dev": true, + "dependencies": { + "object-keys": "~0.4.0" + }, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/gulp-header": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-2.0.12.tgz", + "integrity": "sha512-7PFW56tRISOroZ3N5R+f1Bn4wvdtE5LZXfZqz4ubEAXLEsLS7kSifgsk/lF26hSqnYO786GniQCxCuLxQZ0SoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "concat-with-sourcemaps": "^1.1.0", + "lodash": "^4.17.21", + "map-stream": "^0.0.7", + "through2": "^4.0.2" + } + }, + "node_modules/gulp-header/node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/gulp-if": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz", + "integrity": "sha512-fCUEngzNiEZEK2YuPm+sdMpO6ukb8+/qzbGfJBXyNOXz85bCG7yBI+pPSl+N90d7gnLvMsarthsAImx0qy7BAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "gulp-match": "^1.1.0", + "ternary-stream": "^3.0.0", + "through2": "^3.0.1" + } + }, + "node_modules/gulp-if/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/gulp-less": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-5.0.0.tgz", + "integrity": "sha512-W2I3TewO/By6UZsM/wJG3pyK5M6J0NYmJAAhwYXQHR+38S0iDtZasmUgFCH3CQj+pQYw/PAIzxvFvwtEXz1HhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "less": "^3.7.1 || ^4.0.0", + "object-assign": "^4.0.1", + "plugin-error": "^1.0.0", + "replace-ext": "^2.0.0", + "through2": "^4.0.0", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gulp-less/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/gulp-less/node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/gulp-match": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.1.0.tgz", + "integrity": "sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.3" + } + }, + "node_modules/gulp-minifier": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/gulp-minifier/-/gulp-minifier-3.5.0.tgz", + "integrity": "sha512-ZFjfVuEyfSsLsbREcdaVsldxY/0CLBLurQRHweq79cBs9s1znSS8KPxEm8OKPbUljxfNDDoaYhqpLSAjvDn3LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-css": "^4.2.1", + "html-minifier-terser": "^5.0.4", + "lodash": "^4.17.4", + "plugin-error": "^1.0.0", + "terser": "^4.3.9", + "through2": "^3.0.1", + "vinyl": "^2.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/gulp-minifier/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/gulp-newer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gulp-newer/-/gulp-newer-1.4.0.tgz", + "integrity": "sha512-h79fGO55S/P9eAADbLAP9aTtVYpLSR1ONj08VPaSdVVNVYhTS8p1CO1TW7kEMu+hC+sytmCqcUr5LesvZEtDoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.0.3", + "kew": "^0.7.0", + "plugin-error": "^0.1.2" + } + }, + "node_modules/gulp-newer/node_modules/arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-newer/node_modules/arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-newer/node_modules/array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-newer/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-newer/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/gulp-newer/node_modules/plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/gulp-plumber/-/gulp-plumber-1.2.1.tgz", + "integrity": "sha512-mctAi9msEAG7XzW5ytDVZ9PxWMzzi1pS2rBH7lA095DhMa6KEXjm+St0GOCc567pJKJ/oCvosVAZEpAey0q2eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^1.1.3", + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2", + "through2": "^2.0.3" + }, + "engines": { + "node": ">=0.10", + "npm": ">=1.2.10" + } + }, + "node_modules/gulp-plumber/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-plumber/node_modules/plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/gulp-postcss": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-10.0.0.tgz", + "integrity": "sha512-z1RF2RJEX/BvFsKN11PXai8lRmihZTiHnlJf7Zu8uHaA/Q7Om4IeN8z1NtMAW5OiLwUY02H0DIFl9tHl0CNSgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fancy-log": "^2.0.0", + "plugin-error": "^2.0.1", + "postcss-load-config": "^5.0.0", + "vinyl-sourcemaps-apply": "^0.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/gulp-postcss/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-postcss/node_modules/plugin-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz", + "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-rename": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.1.0.tgz", + "integrity": "sha512-dGuzuH8jQGqCMqC544IEPhs5+O2l+IkdoSZsgd4kY97M1CxQeI3qrmweQBIrxLBbjbe/8uEWK8HHcNBc3OCy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/gulp-sourcemaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", + "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gulp-sourcemaps/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-sourcemaps/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/gulp-terser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gulp-terser/-/gulp-terser-2.1.0.tgz", + "integrity": "sha512-lQ3+JUdHDVISAlUIUSZ/G9Dz/rBQHxOiYDQ70IVWFQeh4b33TC1MCIU+K18w07PS3rq/CVc34aQO4SUbdaNMPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "plugin-error": "^1.0.1", + "terser": "^5.9.0", + "through2": "^4.0.2", + "vinyl-sourcemaps-apply": "^0.2.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gulp-terser/node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/gulp-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/gulp-terser/node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gulp-terser/node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/gulp-typescript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", + "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^3.0.5", + "plugin-error": "^1.0.1", + "source-map": "^0.7.3", + "through2": "^3.0.0", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.3" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev" + } + }, + "node_modules/gulp-typescript/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-typescript/node_modules/fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/fs-mkdirp-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/gulp-typescript/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/gulp-typescript/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/gulp-typescript/node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-typescript/node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", + "dev": true, + "license": "MIT", + "dependencies": { + "flush-write-stream": "^1.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-typescript/node_modules/now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.3.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-typescript/node_modules/resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "value-or-function": "^3.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-typescript/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/gulp-typescript/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/gulp-typescript/node_modules/to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/to-through/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/gulp-typescript/node_modules/value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/vinyl-fs/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/gulp-typescript/node_modules/vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulplog": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "glogg": "^2.2.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "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==", + "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": "BSD-3-Clause" + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "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-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "license": "MIT", + "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/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "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/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/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-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/kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha512-IG6nm0+QtAMdXt9KvbgbGdvY50RSrw+U4sGZg+KlrSKPJEwVE5JVoI3d7RWfSMdBQneRheeAOj3lIjX5VL/9RQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/last-run": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/less": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/less/-/less-4.5.1.tgz", + "integrity": "sha512-UKgI3/KON4u6ngSsnDADsUERqhZknsVZbnuzlRZXLQCmfC/MDld42fTydUE9B+Mla1AL6SJ/Pp6SlEFi/AVGfw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less/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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/liftoff": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.1.tgz", + "integrity": "sha512-wwLXMbuxSF8gMvubFcFRp56lkFV69twvbU5vDPbaw+Q+/rF8j0HKjGbIdlSi+LuJm9jf7k9PB+nTxnsLMPcv2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "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/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-encoder-host": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/media-encoder-host/-/media-encoder-host-9.0.27.tgz", + "integrity": "sha512-4CIiDEXOjgWOLO4RrdC82ivGqZ6H+G6LxDEpJh19eJBKXYC1OXAzOLS0ZGgZrXGoxJ/JlVzLA467J95U90PUEg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "media-encoder-host-broker": "^8.0.25", + "media-encoder-host-worker": "^10.0.25", + "tslib": "^2.8.1" + } + }, + "node_modules/media-encoder-host-broker": { + "version": "8.0.25", + "resolved": "https://registry.npmjs.org/media-encoder-host-broker/-/media-encoder-host-broker-8.0.25.tgz", + "integrity": "sha512-MYimnjPy9G7h9stSCL8Y5HNPv7QkFNsIafyqQ8F+4sMYJvdKzzkpBLibEoXKRdGMKTphuhRVc416mgn/4dfL7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "broker-factory": "^3.1.13", + "fast-unique-numbers": "^9.0.26", + "media-encoder-host-worker": "^10.0.25", + "tslib": "^2.8.1" + } + }, + "node_modules/media-encoder-host-worker": { + "version": "10.0.25", + "resolved": "https://registry.npmjs.org/media-encoder-host-worker/-/media-encoder-host-worker-10.0.25.tgz", + "integrity": "sha512-dot201MppYByCDWoXvVcKz6gD5GMw9DmKvwfW2PryY8sUiPP8EjK9SSYQrG8HJ3d5fNRLfYR7YPBTJqFrn4ZpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "extendable-media-recorder-wav-encoder-broker": "^7.0.125", + "tslib": "^2.8.1", + "worker-factory": "^7.0.48" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "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/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": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/multi-buffer-data-view": { + "version": "6.0.26", + "resolved": "https://registry.npmjs.org/multi-buffer-data-view/-/multi-buffer-data-view-6.0.26.tgz", + "integrity": "sha512-fwXRJkksBByjI20KODPOCRj5/na4YKq8wBx/GOrkSWXA1fyX2pxRBbMfbh/uKSeWA+/d+wp4UNL2RhqHT/ox5Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/mute-stdout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "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/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.assign/node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.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/ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/ordered-read-streams/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ordered-read-streams/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/ordered-read-streams/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true, + "license": "MIT" + }, + "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-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-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/plugin-error/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.1.0.tgz", + "integrity": "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1", + "yaml": "^2.4.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + } + } + }, + "node_modules/postcss-rtl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-rtl/-/postcss-rtl-2.0.0.tgz", + "integrity": "sha512-vFu78CvaGY9BafWRHNgDm6OjUxzRCWWCrp+KtnyXdgwibLwb/j5ls8Z/ubvOsk9B/Q2NLwSPrXRARKMaa9RBmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rtlcss": "4.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-rtl/node_modules/rtlcss": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.0.0.tgz", + "integrity": "sha512-j6oypPP+mgFwDXL1JkLCtm6U/DQntMUqlv5SOhpgHhdIE+PmBcjrtAHIpXfbIup47kD5Sgja9JDsDF1NNOsBwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.6", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/pumpify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/pumpify/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/pumpify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/recorder-audio-worklet": { + "version": "6.0.57", + "resolved": "https://registry.npmjs.org/recorder-audio-worklet/-/recorder-audio-worklet-6.0.57.tgz", + "integrity": "sha512-t0aZn6WgaC5UKlfcwD5gLFCCl5LaIfNPywj1GdgHjlY9rsihGRKE/HlFa94grhVPsnQNreDPyxHsAJXnrOpSdQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "broker-factory": "^3.1.13", + "fast-unique-numbers": "^9.0.26", + "recorder-audio-worklet-processor": "^5.0.39", + "standardized-audio-context": "^25.3.77", + "subscribable-things": "^2.1.57", + "tslib": "^2.8.1", + "worker-factory": "^7.0.48" + } + }, + "node_modules/recorder-audio-worklet-processor": { + "version": "5.0.39", + "resolved": "https://registry.npmjs.org/recorder-audio-worklet-processor/-/recorder-audio-worklet-processor-5.0.39.tgz", + "integrity": "sha512-qGIXJC00M/QXY6eAOfQeRuBVC4HyKaSKbuH3QBNqvvWlgcaq9a37rzA4c8GwiLtLN27H+SciWyJSX0CGTJJoEA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/replace-homedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "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-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/rxjs-interop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rxjs-interop/-/rxjs-interop-2.0.0.tgz", + "integrity": "sha512-ASEq9atUw7lualXB+knvgtvwkCEvGWV2gDD/8qnASzBkzEARZck9JAyxmY8OS6Nc1pCPEgDTKNcx+YqqYfzArw==", + "license": "MIT" + }, + "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==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "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/semver-greatest-satisfied-range": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sver": "^1.8.3" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "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/source-map-support/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/sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/standardized-audio-context": { + "version": "25.3.77", + "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.77.tgz", + "integrity": "sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.6", + "automation-events": "^7.0.9", + "tslib": "^2.7.0" + } + }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.13.2" + } + }, + "node_modules/stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/string-width/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/subscribable-things": { + "version": "2.1.57", + "resolved": "https://registry.npmjs.org/subscribable-things/-/subscribable-things-2.1.57.tgz", + "integrity": "sha512-Ebcu2SJUntGnfJlTKc5jIGcDbuev4Ys2bRstzl5DUyzjWTZV9ymONZ0x8kEiN8NtnDlVuFe40EB8t3XvH8SWkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "rxjs-interop": "^2.0.0", + "tslib": "^2.8.1" + } + }, + "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==", + "dev": true, + "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/sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "semver": "^6.3.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/ternary-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-3.0.0.tgz", + "integrity": "sha512-oIzdi+UL/JdktkT+7KU5tSIQjj8pbShj3OASuvDEhm0NT5lppsm7aXWAmAq4/QMaBIyfuEcNLbAQA+HpaISobQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.1", + "fork-stream": "^0.0.4", + "merge-stream": "^2.0.0", + "through2": "^3.0.1" + } + }, + "node_modules/ternary-stream/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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": { + "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/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undertaker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.4.0.tgz", + "integrity": "sha512-V6QarSfeSgDipGA9EZdoIzu03ZDlOFkk+FbEP5cwgrZXN3iIkYR91IjU2EnM6rB835kGQsqHX8qncObTXV+6KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "3.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/vinyl-contents/node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.2.tgz", + "integrity": "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.3", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.1", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/vinyl-fs/node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/vinyl-sourcemap/node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "source-map": "^0.5.1" + } + }, + "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/worker-factory": { + "version": "7.0.48", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.48.tgz", + "integrity": "sha512-CGmBy3tJvpBPjUvb0t4PrpKubUsfkI1Ohg0/GGFU2RvA9j/tiVYwKU8O7yu7gH06YtzbeJLzdUR29lmZKn5pag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-unique-numbers": "^9.0.26", + "tslib": "^2.8.1" + } + }, + "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==", + "dev": true, + "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/wrap-ansi/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "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/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..9b6657de --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "crestapps.core", + "private": true, + "scripts": { + "build": "gulp build", + "rebuild": "gulp rebuild", + "watch": "gulp watch" + }, + "devDependencies": { + "@babel/core": "^7.22.11", + "@babel/preset-env": "^7.22.10", + "@babel/preset-flow": "7.27.1", + "child_process": "^1.0.2", + "es6-promise": "4.2.8", + "fancy-log": "^2.0.0", + "glob": "^13.0.6", + "graceful-fs": "4.2.11", + "gulp": "^5.0.1", + "gulp-babel": "8.0.0", + "gulp-concat": "2.6.1", + "gulp-dart-sass": "1.1.0", + "gulp-eol": "0.2.0", + "gulp-header": "^2.0.12", + "gulp-if": "3.0.0", + "gulp-less": "5.0.0", + "gulp-minifier": "^3.5.0", + "gulp-newer": "1.4.0", + "gulp-plumber": "1.2.1", + "gulp-postcss": "^10.0.0", + "gulp-rename": "2.1.0", + "gulp-sourcemaps": "^3.0.0", + "gulp-terser": "2.1.0", + "gulp-typescript": "^5.0.1", + "merge-stream": "2.0.0", + "path-posix": "1.0.0", + "postcss": "8.5.6", + "postcss-rtl": "^2.0.0", + "rtlcss": "4.3.0", + "source-map": "^0.7.4", + "typescript": "^5.4.5" + }, + "dependencies": { + "chart.js": "^4.5.1", + "extendable-media-recorder": "^9.2.36" + }, + "overrides": { + "gulp-match": { + "minimatch": "^10.0.1" + }, + "gulp-newer": { + "minimatch": "^10.0.1" + }, + "gulp-typescript": { + "minimatch": "^10.0.1" + }, + "@gulp-sourcemaps/identity-map": { + "postcss": "8.5.6" + } + } +} + diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatMessageCompletedContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatMessageCompletedContext.cs new file mode 100644 index 00000000..b1990bbf --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatMessageCompletedContext.cs @@ -0,0 +1,40 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Provides context for the callback. +/// +public sealed class ChatMessageCompletedContext +{ + /// + /// Gets the AI profile that was used. + /// + public required AIProfile Profile { get; init; } + /// + /// Gets the current chat session. + /// + public required AIChatSession ChatSession { get; init; } + /// + /// Gets the prompts associated with the current chat session. + /// + public required IReadOnlyList Prompts { get; init; } + /// + /// Gets or sets the time in milliseconds the AI took to generate the response. + /// + public double ResponseLatencyMs { get; init; } + /// + /// Gets or sets the number of input tokens used in this completion. + /// + public int InputTokenCount { get; init; } + /// + /// Gets or sets the number of output tokens generated in this completion. + /// + public int OutputTokenCount { get; init; } + + /// + /// Gets a shared item bag that handlers can use to pass host-agnostic results + /// to later handlers without rerunning the same processing logic. + /// + public IDictionary Items { get; } = new Dictionary(StringComparer.Ordinal); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatNotificationActionNames.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatNotificationActionNames.cs new file mode 100644 index 00000000..f9ad3689 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatNotificationActionNames.cs @@ -0,0 +1,10 @@ +namespace CrestApps.Core.AI.Chat; + +/// +/// Well-known notification action names used by built-in action handlers. +/// +public static class ChatNotificationActionNames +{ + public const string CancelTransfer = "cancel-transfer"; + public const string EndSession = "end-session"; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatNotificationTypes.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatNotificationTypes.cs new file mode 100644 index 00000000..a1750378 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/ChatNotificationTypes.cs @@ -0,0 +1,16 @@ +namespace CrestApps.Core.AI.Chat; + +/// +/// Well-known notification types used by built-in chat notifications. Each type serves as +/// both the unique identifier and the CSS styling class for the notification. +/// +public static class ChatNotificationTypes +{ + public const string Typing = "typing"; + public const string Transfer = "transfer"; + public const string AgentConnected = "agent-connected"; + public const string AgentReconnecting = "agent-reconnecting"; + public const string ConnectionLost = "connection-lost"; + public const string ConversationEnded = "conversation-ended"; + public const string SessionEnded = "session-ended"; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionAnalyticsRecorder.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionAnalyticsRecorder.cs new file mode 100644 index 00000000..46ae3b48 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionAnalyticsRecorder.cs @@ -0,0 +1,20 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Persists host-specific chat session analytics after shared post-close analysis +/// has determined the final session metrics and resolution status. +/// +public interface IAIChatSessionAnalyticsRecorder +{ + /// + /// Records end-of-session analytics for the specified chat session. + /// + Task RecordSessionEndedAsync( + AIProfile profile, + AIChatSession session, + IReadOnlyList prompts, + bool isResolved, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionConversionGoalRecorder.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionConversionGoalRecorder.cs new file mode 100644 index 00000000..69b6bd45 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionConversionGoalRecorder.cs @@ -0,0 +1,19 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Persists host-specific conversion-goal evaluation results after the shared +/// post-close processor evaluates the configured goals. +/// +public interface IAIChatSessionConversionGoalRecorder +{ + /// + /// Records evaluated conversion-goal results for the specified chat session. + /// + Task RecordConversionGoalsAsync( + AIProfile profile, + AIChatSession session, + IReadOnlyList goalResults, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionExtractedDataRecorder.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionExtractedDataRecorder.cs new file mode 100644 index 00000000..98476ec2 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionExtractedDataRecorder.cs @@ -0,0 +1,19 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Persists host-specific extracted-data snapshots so extracted chat-session values +/// can be queried later for reporting and analytics. +/// +public interface IAIChatSessionExtractedDataRecorder +{ + /// + /// Records the current extracted-data snapshot for the specified chat session. + /// Implementations should upsert an existing record when one already exists. + /// + Task RecordExtractedDataAsync( + AIProfile profile, + AIChatSession session, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionHandler.cs new file mode 100644 index 00000000..da7a766b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionHandler.cs @@ -0,0 +1,26 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Handles lifecycle events raised during an AI chat session, such as when +/// a message exchange completes. Implementations can perform post-processing +/// tasks like data extraction, analytics, or workflow triggers. +/// Inherits from to support standard +/// lifecycle events (Initializing, Initialized, Creating, Created, Loaded, +/// Deleting, Deleted, Updating, Updated, Validating, Validated). +/// +public interface IAIChatSessionHandler : ICatalogEntryHandler +{ + /// + /// Called after a user message has been processed and the assistant response + /// has been fully generated and appended to the session. + /// + /// + /// The context describing the completed message exchange, including the + /// profile, session, messages, and an scoped + /// to the current request. + /// + Task MessageCompletedAsync(ChatMessageCompletedContext context); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionManager.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionManager.cs new file mode 100644 index 00000000..9e598f6e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IAIChatSessionManager.cs @@ -0,0 +1,81 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Manages AI chat session lifecycle operations including creation, retrieval, +/// pagination, persistence, and deletion of chat sessions tied to AI profiles. +/// +public interface IAIChatSessionManager +{ + /// + /// Asynchronously retrieves an existing AI chat session by its session ID. + /// + /// The unique identifier of the chat session. + /// + /// A task representing the asynchronous operation. The task result is the if found, + /// or null if no session with the specified ID exists. + /// + Task FindByIdAsync(string id); + + /// + /// Asynchronously retrieves an existing AI chat session by its session ID after applying ownership check. + /// + /// The unique identifier of the chat session. Must not be null or empty. + /// + /// A task representing the asynchronous operation. The task result is the if found, + /// or null if no session with the specified session ID exists. + /// + Task FindAsync(string id); + + /// + /// Asynchronously retrieves a list of top AI chat sessions based on the provided pagination parameters and query context. + /// + /// The page number to retrieve (1-based index). Must be greater than 0. + /// The number of sessions to retrieve per page. Must be greater than 0. + /// The context used to filter and order the chat sessions. Must not be null. + /// + /// A task representing the asynchronous operation. The task result is a list of objects, + /// which represent the top sessions based on the query context and pagination parameters. + /// + Task PageAsync(int page, int pageSize, AIChatSessionQueryContext context = null); + + /// + /// Asynchronously creates a new AI chat session for the specified AI chat profile. + /// + /// The AI chat profile for which the new session will be created. Must not be null. + /// The request context + /// + /// A task representing the asynchronous operation. The task result is a new + /// associated with the provided profile. + /// + Task NewAsync(AIProfile profile, NewAIChatSessionContext context); + + /// + /// Asynchronously saves or updates the specified AI chat session. + /// + /// The AI chat session to save or update. Must not be null. + /// + /// A task representing the asynchronous operation. This method does not return any value. + /// + Task SaveAsync(AIChatSession chatSession); + + /// + /// Asynchronously deletes the specified AI chat session. + /// + /// The unique identifier of the chat session to delete. Must not be null or empty. + /// + /// A task representing the asynchronous operation. The task result is true if the session was successfully deleted, + /// or false if the session was not found or could not be deleted. + /// + Task DeleteAsync(string sessionId); + + /// + /// Asynchronously deletes all AI chat sessions for the specified profile and current user. + /// + /// The profile identifier to filter sessions. Must not be null or empty. + /// + /// A task representing the asynchronous operation. The task result is the number of sessions that were deleted. + /// + Task DeleteAllAsync(string profileId); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatInteractionSettingsHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatInteractionSettingsHandler.cs new file mode 100644 index 00000000..b8111eae --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatInteractionSettingsHandler.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Handles lifecycle events raised while saving settings +/// from the client (e.g., SignalR hub). +/// +/// +/// Implementations can enrich, validate, or otherwise mutate the +/// based on the raw settings payload. The hub invokes before +/// core properties are persisted, and after the interaction is saved. +/// +public interface IChatInteractionSettingsHandler +{ + /// + /// Called while the settings are being applied, + /// before the interaction is persisted. + /// + /// The being updated. + /// The raw settings payload from the client. + Task UpdatingAsync(ChatInteraction interaction, JsonElement settings); + + /// + /// Called after the has been persisted. + /// + /// The updated . + /// The raw settings payload from the client. + Task UpdatedAsync(ChatInteraction interaction, JsonElement settings); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationActionHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationActionHandler.cs new file mode 100644 index 00000000..8735d0ff --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationActionHandler.cs @@ -0,0 +1,19 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Handles user-initiated actions on chat system messages. +/// When a user clicks an action button on a notification (e.g., "Cancel Transfer"), +/// the hub resolves a keyed service whose key matches the action name. +/// Register implementations using services.AddKeyedScoped<IChatNotificationActionHandler, YourHandler>("your-action-name"). +/// +public interface IChatNotificationActionHandler +{ + /// + /// Handles the notification action triggered by the user. + /// + /// The context containing session details, notification ID, and service provider. + /// A token to cancel the operation. + Task HandleAsync(ChatNotificationActionContext context, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationSender.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationSender.cs new file mode 100644 index 00000000..21a9ab3d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationSender.cs @@ -0,0 +1,44 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Sends transient UI notifications to chat clients via SignalR. +/// Notifications appear as system messages in the chat interface and are separate +/// from chat history. Use this service from webhooks, background tasks, or response +/// handlers to provide real-time feedback to users. +/// +/// +/// Notifications are sent to SignalR groups, so all clients connected to the +/// same session receive the notification. The group name is determined by the +/// parameter. +/// +public interface IChatNotificationSender +{ + /// + /// Sends a notification to all clients connected to the specified session. + /// If a notification with the same already exists + /// on the client, it will be replaced. + /// + /// The session or interaction identifier. + /// The type of chat context. + /// The notification to display. + Task SendAsync(string sessionId, ChatContextType chatType, ChatNotification notification); + + /// + /// Updates an existing notification on all connected clients. + /// Only replaces the notification if one with a matching exists. + /// + /// The session or interaction identifier. + /// The type of chat context. + /// The updated notification. + Task UpdateAsync(string sessionId, ChatContextType chatType, ChatNotification notification); + + /// + /// Removes a notification from all connected clients. + /// + /// The session or interaction identifier. + /// The type of chat context. + /// The type of the notification to remove. + Task RemoveAsync(string sessionId, ChatContextType chatType, string notificationType); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationTransport.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationTransport.cs new file mode 100644 index 00000000..913b7eab --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IChatNotificationTransport.cs @@ -0,0 +1,46 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Defines the low-level transport for delivering chat notification messages to +/// clients connected to a specific hub. Implementations are registered as keyed +/// services using as the key, enabling the +/// to dispatch notifications without +/// coupling to concrete hub types. +/// +/// +/// +/// Each hub that supports notification system messages should provide its own implementation +/// and register it as a keyed service: +/// +/// +/// services.AddKeyedScoped<IChatNotificationTransport, MyChatNotificationTransport>(ChatContextType.AIChatSession); +/// +/// +public interface IChatNotificationTransport +{ + /// + /// Sends a notification to all clients in the session group. If a notification + /// with the same already exists on the client, + /// it is replaced. + /// + /// The session or interaction identifier. + /// The notification to display. + Task SendNotificationAsync(string sessionId, ChatNotification notification); + + /// + /// Updates an existing notification on all connected clients in the session group. + /// Only replaces the notification if one with a matching exists. + /// + /// The session or interaction identifier. + /// The updated notification. + Task UpdateNotificationAsync(string sessionId, ChatNotification notification); + + /// + /// Removes a notification from all connected clients in the session group. + /// + /// The session or interaction identifier. + /// The type of the notification to remove. + Task RemoveNotificationAsync(string sessionId, string notificationType); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelay.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelay.cs new file mode 100644 index 00000000..3c14dac0 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelay.cs @@ -0,0 +1,65 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Defines the contract for a persistent relay connection to an external system +/// (e.g., a third-party live-agent platform) for real-time bidirectional communication. +/// +/// +/// +/// Unlike the webhook pattern where the external system calls back into your application, +/// an maintains a persistent connection so that events +/// such as typing indicators, agent-connected notifications, wait-time updates, +/// and chat messages flow in real time without polling. +/// +/// +/// This interface is protocol-agnostic. Implementations can use any transport: +/// WebSocket, SSE (Server-Sent Events), gRPC streaming, WebRTC data channels, +/// message queues, event buses, or any other protocol. The relay infrastructure +/// manages connection lifecycles and event routing regardless of the underlying transport. +/// +/// +/// Implementations should handle reconnection logic and graceful shutdown. The relay is +/// managed by , which tracks active relay instances +/// by session ID and disposes them when sessions end. +/// +/// +public interface IExternalChatRelay : IAsyncDisposable +{ + /// + /// Determines whether the relay is currently connected to the external system. + /// Implementations may perform a network request to verify the connection status. + /// + /// A token to cancel the operation. + /// if the relay is connected; otherwise, . + Task IsConnectedAsync(CancellationToken cancellationToken = default); + + /// + /// Establishes the connection to the external system for the given session. + /// + /// The relay context containing session identity and services. + /// A token to cancel the operation. + Task ConnectAsync(ExternalChatRelayContext context, CancellationToken cancellationToken = default); + + /// + /// Sends a user prompt to the external system via the relay connection. + /// + /// The user's message text. + /// A token to cancel the operation. + Task SendPromptAsync(string text, CancellationToken cancellationToken = default); + + /// + /// Sends a signal (e.g., thumbs up, thumbs down, user typing) to the external system. + /// + /// The name of the signal to send. + /// Optional key-value data associated with the signal. + /// A token to cancel the operation. + Task SendSignalAsync(string signalName, IDictionary data = null, CancellationToken cancellationToken = default); + + /// + /// Gracefully disconnects from the external system. + /// + /// A token to cancel the operation. + Task DisconnectAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayEventHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayEventHandler.cs new file mode 100644 index 00000000..e9abb1fd --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayEventHandler.cs @@ -0,0 +1,32 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Handles events received from an . Implementations +/// map relay events to chat notifications, message writes, and other actions. +/// +/// +/// The default implementation resolves a keyed +/// by and delegates to +/// to send/remove notifications. +/// To handle custom event types, register a keyed builder: +/// +/// services.AddKeyedScoped<IExternalChatRelayNotificationBuilder, MyBuilder>("my-event-type"); +/// +/// +public interface IExternalChatRelayEventHandler +{ + /// + /// Processes an event received from the external relay. + /// + /// The session or interaction identifier. + /// The type of chat context. + /// The event received from the external system. + /// A token to cancel the operation. + Task HandleEventAsync( + string sessionId, + ChatContextType chatType, + ExternalChatRelayEvent relayEvent, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayManager.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayManager.cs new file mode 100644 index 00000000..821be65e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayManager.cs @@ -0,0 +1,50 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Manages the lifecycle of instances, tracking active +/// relay connections by session ID. This service is registered as a singleton so that +/// relay connections persist across scoped service lifetimes. +/// +/// +/// +/// Use this manager to retrieve an existing relay for a session or create a new one. +/// When a session ends or the relay is no longer needed, call +/// to gracefully disconnect and dispose of the relay. +/// +/// +public interface IExternalChatRelayManager +{ + /// + /// Gets an existing relay for the specified session, or creates and connects a new one + /// using the provided factory function. + /// + /// The session identifier. + /// The relay context for establishing a new connection. + /// + /// A factory function that creates a new instance + /// when no existing relay is found for the session. + /// + /// A token to cancel the operation. + /// The active relay instance for the session. + Task GetOrCreateAsync( + string sessionId, + ExternalChatRelayContext context, + Func factory, + CancellationToken cancellationToken = default); + + /// + /// Gets an existing relay for the specified session, or if none exists. + /// + /// The session identifier. + /// The active relay instance, or . + IExternalChatRelay Get(string sessionId); + + /// + /// Gracefully disconnects and removes the relay for the specified session. + /// + /// The session identifier. + /// A token to cancel the operation. + Task CloseAsync(string sessionId, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayNotificationBuilder.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayNotificationBuilder.cs new file mode 100644 index 00000000..0e0a6641 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayNotificationBuilder.cs @@ -0,0 +1,50 @@ +using CrestApps.Core.AI.Models; +using Microsoft.Extensions.Localization; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Populates a and an +/// for a specific external chat relay event type. +/// Implementations are registered as keyed scoped services where the key is the event type string +/// (e.g., ). +/// +/// +/// +/// The DefaultExternalChatRelayEventHandler resolves the builder by event type key, +/// creates the with set from +/// , and then calls to populate remaining properties. +/// Builders should not modify — this allows the handler to control +/// the type and enables multiple builders to contribute without overriding each other. +/// +/// +/// To handle a custom event type, register a keyed builder: +/// +/// services.AddKeyedScoped<IExternalChatRelayNotificationBuilder, MyCustomBuilder>("my-custom-event"); +/// +/// +/// +public interface IExternalChatRelayNotificationBuilder +{ + /// + /// Gets the notification type that this builder produces. The type serves as both the + /// unique identifier and the CSS styling class. Use constants from + /// for built-in types, or any custom string. + /// This value is used by the handler to create the with its + /// pre-set. Builders should not modify the type in + /// . + /// Return for removal-only builders that do not send a notification. + /// + string NotificationType { get; } + + /// + /// Populates the notification and result for the given relay event. + /// The is already set by the handler via + /// — do not override it in this method. + /// + /// The event received from the external system. + /// The notification object to populate with content, icon, etc. + /// The result to configure with notification types to remove. + /// The string localizer for translating user-facing messages. + void Build(ExternalChatRelayEvent relayEvent, ChatNotification notification, ExternalChatRelayNotificationResult result, IStringLocalizer T); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayNotificationHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayNotificationHandler.cs new file mode 100644 index 00000000..ab8ffc2d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Chat/IExternalChatRelayNotificationHandler.cs @@ -0,0 +1,30 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Chat; + +/// +/// Handles sending and removing chat notifications described by an +/// . This is the "handler" half of +/// the builder/handler pattern used by the relay event system. +/// +/// +/// The default implementation removes all notifications listed in +/// and then sends the +/// (if present) via +/// . +/// +public interface IExternalChatRelayNotificationHandler +{ + /// + /// Processes a notification result by removing and/or sending notifications. + /// + /// The session or interaction identifier. + /// The type of chat context. + /// The builder result describing which notifications to remove/send. + /// A token to cancel the operation. + Task HandleAsync( + string sessionId, + ChatContextType chatType, + ExternalChatRelayNotificationResult result, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAIClientFactory.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAIClientFactory.cs new file mode 100644 index 00000000..e0a46ca1 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAIClientFactory.cs @@ -0,0 +1,104 @@ +using CrestApps.Core.AI.Models; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Clients; +/// +/// Defines a factory for creating AI clients, such as chat clients and embedding generators, +/// based on the specified provider, connection, and deployment names. +/// +public interface IAIClientFactory +{ + /// + /// Asynchronously creates an instance for the given provider, connection, and deployment. + /// + /// The name of the AI provider (e.g., "OpenAI", "AzureOpenAI"). + /// The name of the connection configuration to use. + /// The name of the deployment or model to use. If not provided, the default embedding deployment from the connection will be used. + /// + /// A representing the asynchronous operation, with the created . + /// + ValueTask CreateChatClientAsync(string providerName, string connectionName, string deploymentName); + /// + /// Asynchronously creates an instance for the given provider, connection, and deployment. + /// + /// The name of the AI provider (e.g., "OpenAI", "AzureOpenAI"). + /// The name of the connection configuration to use. + /// The name of the deployment or model to use. + /// + /// A representing the asynchronous operation, with the created . + /// + ValueTask>> CreateEmbeddingGeneratorAsync(string providerName, string connectionName, string deploymentName = null); + /// + /// Asynchronously creates an instance for the given provider, connection, and deployment. + /// + /// The name of the AI provider (e.g., "OpenAI", "AzureOpenAI"). + /// The name of the connection configuration to use. + /// The name of the deployment or model to use. If not provided, the default images deployment from the connection will be used. + /// + /// A representing the asynchronous operation, with the created . + /// + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + ValueTask CreateImageGeneratorAsync(string providerName, string connectionName, string deploymentName = null); +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + /// + /// Asynchronously creates an instance for the given provider, connection, and deployment. + /// + /// The name of the AI provider (e.g., "OpenAI", "AzureOpenAI"). + /// The name of the connection configuration to use. + /// The name of the deployment or model to use. If not provided, the default speech-to-text deployment from the connection will be used. + /// + /// A representing the asynchronous operation, with the created . + /// + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + ValueTask CreateSpeechToTextClientAsync(string providerName, string connectionName, string deploymentName = null); +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + /// + /// Asynchronously creates an instance from a deployment that may use + /// either a connection reference or contained connection parameters. + /// + /// The AI deployment containing provider, connection, and model information. + /// + /// A representing the asynchronous operation, with the created . + /// + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + ValueTask CreateSpeechToTextClientAsync(AIDeployment deployment); +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + /// + /// Asynchronously creates an instance for the given provider, connection, and deployment. + /// + /// The name of the AI provider (e.g., "AzureSpeech"). + /// The name of the connection configuration to use. + /// The name of the deployment or model to use. If not provided, the default text-to-speech deployment from the connection will be used. + /// + /// A representing the asynchronous operation, with the created . + /// + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + ValueTask CreateTextToSpeechClientAsync(string providerName, string connectionName, string deploymentName = null); +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + /// + /// Asynchronously creates an instance from a deployment that may use + /// either a connection reference or contained connection parameters. + /// + /// The AI deployment containing provider, connection, and model information. + /// + /// A representing the asynchronous operation, with the created . + /// + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + ValueTask CreateTextToSpeechClientAsync(AIDeployment deployment); +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAIClientProvider.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAIClientProvider.cs new file mode 100644 index 00000000..44f6de0c --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAIClientProvider.cs @@ -0,0 +1,73 @@ +using CrestApps.Core.AI.Models; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Clients; +/// +/// Provides methods to obtain AI chat clients and embedding generators for specific providers. +/// +public interface IAIClientProvider +{ + /// + /// Determines whether this provider can handle the specified provider name. + /// + /// The name of the provider to check. + /// true if the provider can be handled; otherwise, false. + bool CanHandle(string providerName); + /// + /// Gets an AI chat client for the specified connection and deployment. + /// + /// The connection entry containing provider configuration. + /// The optional deployment name to use. + /// A representing the asynchronous operation. + ValueTask GetChatClientAsync(AIProviderConnectionEntry connection, string deploymentName = null); + /// + /// Gets an embedding generator for the specified connection and deployment. + /// + /// The connection entry containing provider configuration. + /// The optional deployment name to use. + /// A representing the asynchronous operation. + ValueTask>> GetEmbeddingGeneratorAsync(AIProviderConnectionEntry connection, string deploymentName = null); + /// + /// Gets an image generator for the specified connection and deployment. + /// + /// The connection entry containing provider configuration. + /// The optional deployment name to use. + /// A representing the asynchronous operation. + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + ValueTask GetImageGeneratorAsync(AIProviderConnectionEntry connection, string deploymentName = null); +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + /// + /// Gets a speech-to-text client for the specified connection and deployment. + /// + /// The connection entry containing provider configuration. + /// The optional deployment name to use. + /// A representing the asynchronous operation. + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + ValueTask GetSpeechToTextClientAsync(AIProviderConnectionEntry connection, string deploymentName = null); +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + /// + /// Gets a text-to-speech client for the specified connection and deployment. + /// + /// The connection entry containing provider configuration. + /// The optional deployment name to use. + /// A representing the asynchronous operation. + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + ValueTask GetTextToSpeechClientAsync(AIProviderConnectionEntry connection, string deploymentName = null); +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + /// + /// Gets the available speech voices for the specified connection and deployment. + /// + /// The connection entry containing provider configuration. + /// The optional deployment name to use. + /// An array of available instances. + Task GetSpeechVoicesAsync(AIProviderConnectionEntry connection, string deploymentName = null); +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAICompletionClient.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAICompletionClient.cs new file mode 100644 index 00000000..60f95f3f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Clients/IAICompletionClient.cs @@ -0,0 +1,38 @@ +using CrestApps.Core.AI.Models; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Clients; + +/// +/// Defines an AI completion client that communicates with a specific AI provider +/// to generate chat completions and streaming responses. Each implementation +/// targets a different AI backend (e.g., OpenAI, Azure OpenAI, Ollama). +/// +public interface IAICompletionClient +{ + /// + /// Gets the unique technical name of the chat completion client implementation. + /// This name is used to distinguish between different implementations of the clients. + /// Each implementation should return a distinct name that identifies it clearly. + /// + string Name { get; } + + /// + /// Sends a series of messages to the AI chat service and returns the completion response. + /// This method allows communication with the AI chat API by providing input messages and context. + /// + /// A collection of messages that are part of the chat conversation. + /// The context that may provide additional parameters or configurations for the chat request. + /// A task representing the asynchronous operation, with the completion response as the result. + Task CompleteAsync(IEnumerable messages, AICompletionContext context, CancellationToken cancellationToken = default); + + /// + /// Streams chat completion updates from the AI service in real time. + /// This method allows for handling partial responses as they are generated by the AI model. + /// + /// A list of chat messages that define the conversation history. + /// Additional context or parameters for configuring the AI request. + /// A token to cancel the streaming operation if needed. + /// An asynchronous stream of chat completion updates, allowing real-time processing of AI responses. + IAsyncEnumerable CompleteStreamingAsync(IEnumerable messages, AICompletionContext context, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/AICompletionContextKeys.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/AICompletionContextKeys.cs new file mode 100644 index 00000000..df998a74 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/AICompletionContextKeys.cs @@ -0,0 +1,36 @@ +using CrestApps.Core.AI.Tooling; + +namespace CrestApps.Core.AI.Completions; + +/// +/// Provides well-known keys for . +/// These keys are used by orchestration handlers to communicate context availability +/// to downstream consumers such as implementations. +/// +public static class AICompletionContextKeys +{ + public const string CompletionContext = "CompletionContext"; + + public const string Session = "Session"; + + public const string Interaction = "Interaction"; + + public const string InteractionId = "InteractionId"; + + public const string ClientName = "ClientName"; + + /// + /// When set to in , + /// indicates that documents are available for the current session. This enables + /// document processing system tools (e.g., search_documents, list_documents) + /// to be included in the tool registry. + /// + public const string HasDocuments = "HasDocuments"; + + /// + /// When set to in , + /// indicates that authenticated user memory is available for the current request. This enables + /// memory-related system tools to be included in the tool registry. + /// + public const string HasMemory = "HasMemory"; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionContextBuilder.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionContextBuilder.cs new file mode 100644 index 00000000..0e987706 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionContextBuilder.cs @@ -0,0 +1,28 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Completions; + +/// +/// Builds instances from a given resource object. +/// +/// +/// The default implementation creates an empty context, then executes the +/// registered pipeline in the following order: +///1) , +///2) the optional delegate, +///3) . +/// +public interface IAICompletionContextBuilder +{ + /// + /// Creates and configures a new based on the provided . + /// + /// The resource object (e.g., or ChatInteraction) used to seed and configure the completion context. Must not be . + /// An optional delegate to override or fine-tune the context after handlers have run BuildingAsync but before BuiltAsync. + /// A task that completes with the fully built . + /// Thrown if is . + /// + /// + /// + ValueTask BuildAsync(object resource, Action configure = null); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionContextBuilderHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionContextBuilderHandler.cs new file mode 100644 index 00000000..79ae7e1d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionContextBuilderHandler.cs @@ -0,0 +1,32 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Completions; + +/// +/// Handles lifecycle events raised while building an . +/// +/// +/// Implementations can enrich, validate, or otherwise mutate the context. The builder will invoke +/// first, then apply any caller-provided +/// configuration, and finally invoke . +/// Handlers are resolved from DI and executed in reverse registration order to allow last-registered +/// handlers to run first. +/// +public interface IAICompletionContextBuilderHandler +{ + /// + /// Called while the is being constructed, before the optional caller + /// configuration delegate is applied. + /// + /// Carries both the source resource and the mutable . + /// A task that completes when the mutation or validation is done. + Task BuildingAsync(AICompletionContextBuildingContext context); + + /// + /// Called after the context has been fully constructed and the optional caller configuration delegate + /// has been applied. + /// + /// Carries the final along with the source resource. + /// A task that completes when post-build processing is done. + Task BuiltAsync(AICompletionContextBuiltContext context); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionHandler.cs new file mode 100644 index 00000000..01a3521b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionHandler.cs @@ -0,0 +1,25 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Completions; + +/// +/// Handles events raised during AI completion processing, such as when a message +/// or streaming update is received from the AI provider. Implementations can +/// perform logging, analytics, response enrichment, or other post-processing. +/// +public interface IAICompletionHandler +{ + /// + /// Handles a received message asynchronously. + /// + /// The context containing details of the received message. + /// A task that represents the asynchronous operation. + Task ReceivedMessageAsync(ReceivedMessageContext context); + + /// + /// Handles a received update asynchronously. + /// + /// The context containing details of the received update. + /// A task that represents the asynchronous operation. + Task ReceivedUpdateAsync(ReceivedUpdateContext context); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionService.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionService.cs new file mode 100644 index 00000000..9189db95 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionService.cs @@ -0,0 +1,32 @@ +using CrestApps.Core.AI.Clients; +using CrestApps.Core.AI.Models; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Completions; + +/// +/// Provides a deployment-aware facade for generating AI chat completions. +/// Routes completion requests to the appropriate +/// based on the specified . +/// +public interface IAICompletionService +{ + /// + /// Sends a series of messages to the AI chat service and returns the completion response. + /// + /// The deployment that identifies which AI client and model to use. + /// A collection of messages that are part of the chat conversation. + /// The context that may provide additional parameters or configurations for the chat request. + /// A task representing the asynchronous operation, with the completion response as the result. + Task CompleteAsync(AIDeployment deployment, IEnumerable messages, AICompletionContext context, CancellationToken cancellationToken = default); + + /// + /// Streams chat completion updates from the AI service in real time. + /// + /// The deployment that identifies which AI client and model to use. + /// A list of chat messages that define the conversation history. + /// Additional context or parameters for configuring the AI request. + /// A token to cancel the streaming operation if needed. + /// An asynchronous stream of chat completion updates, allowing real-time processing of AI responses. + IAsyncEnumerable CompleteStreamingAsync(AIDeployment deployment, IEnumerable messages, AICompletionContext context, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionServiceHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionServiceHandler.cs new file mode 100644 index 00000000..6075655b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionServiceHandler.cs @@ -0,0 +1,21 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Completions; + +/// +/// Handles per-request configuration of AI completion options, allowing dynamic +/// customization of model parameters, tool selections, and other settings +/// before a completion request is sent to the AI provider. +/// +public interface IAICompletionServiceHandler +{ + /// + /// Called on every request to configure the in the . + /// This allows dynamic customization of the completion behavior depending on the request context. + /// + /// + /// The that provides access to request-specific options and settings. + /// + /// A task that represents the asynchronous operation. + Task ConfigureAsync(CompletionServiceConfigureContext context); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionUsageObserver.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionUsageObserver.cs new file mode 100644 index 00000000..8545d095 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Completions/IAICompletionUsageObserver.cs @@ -0,0 +1,8 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Completions; + +public interface IAICompletionUsageObserver +{ + Task UsageRecordedAsync(AICompletionUsageRecord record, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/CrestApps.Core.AI.Abstractions.csproj b/src/Abstractions/CrestApps.Core.AI.Abstractions/CrestApps.Core.AI.Abstractions.csproj new file mode 100644 index 00000000..b85aa77f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/CrestApps.Core.AI.Abstractions.csproj @@ -0,0 +1,26 @@ + + + + CrestApps.Core.AI + CrestApps AI Abstractions + + $(CrestAppsDescription) + + Core AI abstractions for CrestApps services. Framework-independent, usable in any ASP.NET Core application. + + $(PackageTags) AI Abstractions + + + + + + + + + + + + + + + diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/DataSources/IAIDataSourceStore.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/DataSources/IAIDataSourceStore.cs new file mode 100644 index 00000000..fc9210ba --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/DataSources/IAIDataSourceStore.cs @@ -0,0 +1,11 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.DataSources; + +/// +/// Store for managing records. +/// +public interface IAIDataSourceStore : ICatalog +{ +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/AIDeploymentManagerExtensions.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/AIDeploymentManagerExtensions.cs new file mode 100644 index 00000000..cca147d8 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/AIDeploymentManagerExtensions.cs @@ -0,0 +1,55 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Deployments; + +public static class AIDeploymentManagerExtensions +{ + public static async ValueTask ResolveUtilityOrDefaultAsync( + this IAIDeploymentManager deploymentManager, + string utilityDeploymentName = null, + string chatDeploymentName = null, + string clientName = null, + string connectionName = null) + { + ArgumentNullException.ThrowIfNull(deploymentManager); + + return await deploymentManager.ResolveOrDefaultAsync( + AIDeploymentType.Utility, + utilityDeploymentName, + clientName, + connectionName) + ?? await deploymentManager.ResolveOrDefaultAsync( + AIDeploymentType.Chat, + chatDeploymentName, + clientName, + connectionName); + } + + public static async ValueTask ResolveAsync( + this IAIDeploymentManager deploymentManager, + AIDeploymentType type, + string deploymentName = null, + string clientName = null, + string connectionName = null) + { + ArgumentNullException.ThrowIfNull(deploymentManager); + + var deployment = await deploymentManager.ResolveOrDefaultAsync(type, deploymentName, clientName, connectionName); + + return deployment ?? throw new InvalidOperationException($"Unable to resolve an AI deployment for type '{type}' with deploymentName '{deploymentName ?? "(null)"}', clientName '{clientName ?? "(null)"}', and connectionName '{connectionName ?? "(null)"}'."); + } + + public static async ValueTask ResolveUtilityAsync( + this IAIDeploymentManager deploymentManager, + string utilityDeploymentName = null, + string chatDeploymentName = null, + string clientName = null, + string connectionName = null) + { + ArgumentNullException.ThrowIfNull(deploymentManager); + + var deployment = await deploymentManager.ResolveUtilityOrDefaultAsync(utilityDeploymentName, chatDeploymentName, clientName, connectionName); + + return deployment ?? throw new InvalidOperationException($"Unable to resolve a utility AI deployment using utilityDeploymentName '{utilityDeploymentName ?? "(null)"}', chatDeploymentName '{chatDeploymentName ?? "(null)"}', clientName '{clientName ?? "(null)"}', and connectionName '{connectionName ?? "(null)"}'."); + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentManager.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentManager.cs new file mode 100644 index 00000000..55c2e87a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentManager.cs @@ -0,0 +1,55 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Deployments; + +/// +/// Manages AI deployments with CRUD operations, composite name/source lookup, +/// type-filtered retrieval, and a multi-level fallback resolution chain for +/// selecting the appropriate deployment for a given request. +/// +public interface IAIDeploymentManager : INamedSourceCatalogManager +{ + /// + /// Asynchronously retrieves a list of model deployments for the specified client and connection name. + /// + /// The name of the client. Must not be null or empty. + /// The name of the connection. Must not be null or empty. + /// + /// A ValueTask that represents the asynchronous operation. The result is an + /// containing the model deployments for the specified client and connection. + /// + ValueTask> GetAllAsync(string clientName, string connectionName); + + /// + /// Asynchronously retrieves all deployments supporting the specified type. + /// + /// The deployment type to filter by. + /// + /// A ValueTask that represents the asynchronous operation. The result is an + /// containing all deployments matching the specified type. + /// + ValueTask> GetByTypeAsync(AIDeploymentType type); + + /// + /// Resolves the default deployment of a given type for a specific connection. + /// Returns the deployment marked as IsDefault for that type on the connection, + /// or the first deployment supporting that type on the connection if none is marked as default. + /// + ValueTask GetDefaultAsync(string clientName, string connectionName, AIDeploymentType type); + + /// + /// Resolves a deployment using the full fallback chain: + /// 1. If deploymentId is provided, returns that specific deployment. + /// 2. Falls back to the global default deployment for the given type (from DefaultAIDeploymentSettings). + /// 3. Falls back to the first deployment supporting the requested type within the current scope. + /// Returns if no deployment can be resolved. + /// + ValueTask ResolveOrDefaultAsync(AIDeploymentType type, string deploymentName = null, string clientName = null, string connectionName = null); + + /// + /// Gets all deployments of a given type, optionally filtered by client. + /// Results are suitable for dropdown population, grouped by connection. + /// + ValueTask> GetAllByTypeAsync(AIDeploymentType type, string clientName = null); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentStore.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentStore.cs new file mode 100644 index 00000000..6d9d6c88 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentStore.cs @@ -0,0 +1,12 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Deployments; + +/// +/// Represents the host-specific persisted AI deployment catalog before any configuration-backed +/// deployments are merged into read operations. +/// +public interface IAIDeploymentStore : INamedSourceCatalog +{ +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Exceptions/NoRegisteredCompletionClient.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Exceptions/NoRegisteredCompletionClient.cs new file mode 100644 index 00000000..82b87770 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Exceptions/NoRegisteredCompletionClient.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.AI.Exceptions; + +public class UnregisteredCompletionClientException : Exception +{ + public UnregisteredCompletionClientException(string clientName) + : base($"No registered completion client was found to match '{clientName}'.") + { + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Json/AIProviderConnectionConverter.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Json/AIProviderConnectionConverter.cs new file mode 100644 index 00000000..2e49130e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Json/AIProviderConnectionConverter.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Json; + +public sealed class AIProviderConnectionConverter : JsonConverter +{ + /// + /// Maps legacy configuration key names to their current equivalents. + /// + private static readonly Dictionary _legacyKeyMappings = new(StringComparer.OrdinalIgnoreCase) + { + ["DefaultDeploymentName"] = "ChatDeploymentName", + ["DefaultChatDeploymentName"] = "ChatDeploymentName", + ["DefaultUtilityDeploymentName"] = "UtilityDeploymentName", + ["DefaultEmbeddingDeploymentName"] = "EmbeddingDeploymentName", + ["DefaultImagesDeploymentName"] = "ImagesDeploymentName", + }; + + public override AIProviderConnectionEntry Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Deserialize into a dictionary first. + var dictionary = JsonSerializer.Deserialize>(ref reader, options); + + if (dictionary is null) + { + return null; + } + + // Migrate legacy keys to current keys. + foreach (var (legacyKey, newKey) in _legacyKeyMappings) + { + if (dictionary.TryGetValue(legacyKey, out var value) && !dictionary.ContainsKey(newKey)) + { + dictionary[newKey] = value; + dictionary.Remove(legacyKey); + } + } + + return new AIProviderConnectionEntry(dictionary); + } + + public override void Write(Utf8JsonWriter writer, AIProviderConnectionEntry value, JsonSerializerOptions options) + { + // Serialize as dictionary. + JsonSerializer.Serialize(writer, (IDictionary)value, options); + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Json/AIProviderConnectionJsonConverter.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Json/AIProviderConnectionJsonConverter.cs new file mode 100644 index 00000000..e99f231e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Json/AIProviderConnectionJsonConverter.cs @@ -0,0 +1,117 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Json; + +public sealed class AIProviderConnectionJsonConverter : JsonConverter +{ + public override AIProviderConnection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var node = JsonNode.Parse(ref reader)?.AsObject(); + + if (node == null) + { + return null; + } + + var connection = new AIProviderConnection + { + ItemId = GetString(node, nameof(AIProviderConnection.ItemId)), + Source = GetString(node, nameof(AIProviderConnection.Source)) + ?? GetString(node, nameof(AIProviderConnection.ClientName)) + ?? GetString(node, "ProviderName"), + Name = GetString(node, nameof(AIProviderConnection.Name)), + DisplayText = GetString(node, nameof(AIProviderConnection.DisplayText)), +#pragma warning disable CS0618 // Obsolete deployment name fields retained for backward compatibility + ChatDeploymentName = GetString(node, nameof(AIProviderConnection.ChatDeploymentName)) + ?? GetString(node, "DefaultDeploymentName"), + EmbeddingDeploymentName = GetString(node, nameof(AIProviderConnection.EmbeddingDeploymentName)) + ?? GetString(node, "DefaultEmbeddingDeploymentName"), + ImagesDeploymentName = GetString(node, nameof(AIProviderConnection.ImagesDeploymentName)) + ?? GetString(node, "DefaultImagesDeploymentName"), + UtilityDeploymentName = GetString(node, nameof(AIProviderConnection.UtilityDeploymentName)) + ?? GetString(node, "DefaultUtilityDeploymentName"), +#pragma warning restore CS0618 + CreatedUtc = GetDateTime(node, nameof(AIProviderConnection.CreatedUtc)), + Author = GetString(node, nameof(AIProviderConnection.Author)), + OwnerId = GetString(node, nameof(AIProviderConnection.OwnerId)), + }; + + if (node.TryGetPropertyValue(nameof(AIProviderConnection.Properties), out var propertiesNode) + && propertiesNode is JsonObject properties) + { + // Detach from parent before deserializing. + node.Remove(nameof(AIProviderConnection.Properties)); + connection.Properties = properties.Deserialize>() ?? new Dictionary(); + } + + return connection; + } + + public override void Write(Utf8JsonWriter writer, AIProviderConnection value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + WriteString(writer, nameof(AIProviderConnection.ItemId), value.ItemId); + WriteString(writer, nameof(AIProviderConnection.ClientName), value.ClientName); + WriteString(writer, nameof(AIProviderConnection.Name), value.Name); + WriteString(writer, nameof(AIProviderConnection.DisplayText), value.DisplayText); +#pragma warning disable CS0618 // Obsolete deployment name fields retained for backward compatibility + WriteString(writer, nameof(AIProviderConnection.ChatDeploymentName), value.ChatDeploymentName); + WriteString(writer, nameof(AIProviderConnection.EmbeddingDeploymentName), value.EmbeddingDeploymentName); + WriteString(writer, nameof(AIProviderConnection.ImagesDeploymentName), value.ImagesDeploymentName); + WriteString(writer, nameof(AIProviderConnection.UtilityDeploymentName), value.UtilityDeploymentName); +#pragma warning restore CS0618 + writer.WriteString(nameof(AIProviderConnection.CreatedUtc), value.CreatedUtc); + WriteString(writer, nameof(AIProviderConnection.Author), value.Author); + WriteString(writer, nameof(AIProviderConnection.OwnerId), value.OwnerId); + + writer.WritePropertyName(nameof(AIProviderConnection.Properties)); + + if (value.Properties != null) + { + JsonSerializer.Serialize(writer, value.Properties, options); + } + else + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + + writer.WriteEndObject(); + } + + private static string GetString(JsonObject node, string name) + { + if (node.TryGetPropertyValue(name, out var value) && value != null) + { + return value.GetValue(); + } + + return null; + } + + private static DateTime GetDateTime(JsonObject node, string name) + { + if (node.TryGetPropertyValue(name, out var value) && value != null) + { + return value.GetValue(); + } + + return default; + } + + private static void WriteString(Utf8JsonWriter writer, string name, string value) + { + if (value != null) + { + writer.WriteString(name, value); + } + else + { + writer.WriteNull(name); + } + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemorySafetyService.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemorySafetyService.cs new file mode 100644 index 00000000..9dcd9906 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemorySafetyService.cs @@ -0,0 +1,19 @@ +namespace CrestApps.Core.AI.Memory; + +/// +/// Validates AI memory entries for safety, ensuring that names, descriptions, +/// and content do not contain harmful, disallowed, or policy-violating material +/// before they are persisted. +/// +public interface IAIMemorySafetyService +{ + /// + /// Validates the specified memory entry fields for safety and policy compliance. + /// + /// The name of the memory entry to validate. + /// The description of the memory entry to validate. + /// The content of the memory entry to validate. + /// When validation fails, contains the error message describing the violation. + /// if the entry passes safety validation; otherwise, . + bool TryValidate(string name, string description, string content, out string errorMessage); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemorySearchService.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemorySearchService.cs new file mode 100644 index 00000000..07c0bc73 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemorySearchService.cs @@ -0,0 +1,17 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Memory; + +/// +/// Searches the current user's durable memory entries for relevant context. +/// Hosts can provide their own backing implementation while reusing the shared +/// orchestration and tool behavior. +/// +public interface IAIMemorySearchService +{ + Task> SearchAsync( + string userId, + IEnumerable queries, + int? requestedTopN, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemoryStore.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemoryStore.cs new file mode 100644 index 00000000..056bf1dd --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IAIMemoryStore.cs @@ -0,0 +1,35 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Memory; + +/// +/// Provides persistent storage for AI memory entries, supporting per-user CRUD +/// operations and lookup. Memory entries allow AI sessions to recall user-specific +/// facts across conversations. +/// +public interface IAIMemoryStore : ICatalog +{ + /// + /// Asynchronously counts the number of memory entries owned by the specified user. + /// + /// The unique identifier of the user. + /// The total number of memory entries for the user. + Task CountByUserAsync(string userId); + + /// + /// Asynchronously finds a memory entry by the owning user and entry name. + /// + /// The unique identifier of the user. + /// The unique name of the memory entry within the user scope. + /// The matching entry, or if not found. + Task FindByUserAndNameAsync(string userId, string name); + + /// + /// Asynchronously retrieves memory entries owned by the specified user, up to the given limit. + /// + /// The unique identifier of the user. + /// The maximum number of entries to return. Defaults to 100. + /// A read-only collection of the user's memory entries. + Task> GetByUserAsync(string userId, int limit = 100); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IMemoryVectorSearchService.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IMemoryVectorSearchService.cs new file mode 100644 index 00000000..9dec2e2c --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Memory/IMemoryVectorSearchService.cs @@ -0,0 +1,14 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Infrastructure.Indexing.Models; + +namespace CrestApps.Core.AI.Memory; + +public interface IMemoryVectorSearchService +{ + Task> SearchAsync( + SearchIndexProfile indexProfile, + float[] embedding, + string userId, + int topN, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatProfileSettings.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatProfileSettings.cs new file mode 100644 index 00000000..f8128c36 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatProfileSettings.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.AI.Models; + +public class AIChatProfileSettings +{ + /// + /// Gets or sets a value indicating whether the profile is visible on the admin menu. This is only applicable to profiles with Chat type. + /// + public bool IsOnAdminMenu { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSession.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSession.cs new file mode 100644 index 00000000..52a9f7e3 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSession.cs @@ -0,0 +1,99 @@ +using CrestApps.Core.AI.ResponseHandling; + +namespace CrestApps.Core.AI.Models; + +public sealed class AIChatSession : ExtensibleEntity +{ + /// + /// Gets or sets the unique identifier for the chat session. + /// This property is used to track and manage the session across its lifecycle. + /// + public string SessionId { get; set; } + /// + /// Gets or sets the profile identifier associated with this chat session. + /// It references the user's or client's profile during the session. + /// + public string ProfileId { get; set; } + /// + /// Gets or sets the title of the chat session. + /// This can be a descriptive name or label for the session, such as "Customer Support Chat". + /// + public string Title { get; set; } + /// + /// Gets or sets the user identifier who created this session. + /// This is used to associate the session with a specific user. If unavailable, is used instead. + /// + public string UserId { get; set; } + /// + /// Gets or sets the client identifier who created this session when is not available. + /// This is typically used for cases where the session is initiated by a client or service instead of a specific user. + /// + public string ClientId { get; set; } + /// + /// Gets or sets the collection of document references attached to this session. + /// Documents are uploaded by users and used for RAG (Retrieval-Augmented Generation). + /// + public List Documents { get; set; } = []; + /// + /// Gets or sets the UTC date and time when the session was first created. + /// This property helps track the start time of the session in a standardized format (UTC). + /// + public DateTime CreatedUtc { get; set; } + /// + /// Gets or sets the UTC date and time of the last activity in this session. + /// + public DateTime LastActivityUtc { get; set; } + /// + /// Gets or sets the UTC date and time when the session was closed due to inactivity. + /// + public DateTime? ClosedAtUtc { get; set; } + /// + /// Gets or sets the status of the chat session. + /// + public ChatSessionStatus Status { get; set; } + /// + /// Gets or sets the technical name of the currently + /// handling prompts for this session. When or empty, the default + /// AI handler is used. This value can be changed mid-conversation (e.g., by an AI + /// function that transfers the chat to a live-agent platform). + /// + public string ResponseHandlerName { get; set; } + /// + /// Gets or sets the extracted data fields for this session. + /// Keys are field names from the data extraction configuration. + /// + public Dictionary ExtractedData { get; set; } = []; + /// + /// Gets or sets the results of post-session processing tasks. + /// Keys are task names from the post-session processing configuration. + /// Populated after the session is closed. + /// + public Dictionary PostSessionResults { get; set; } = []; + /// + /// Gets or sets the status of post-session processing for this session. + /// + public PostSessionProcessingStatus PostSessionProcessingStatus { get; set; } + /// + /// Gets or sets the number of attempts made to process post-session tasks. + /// + public int PostSessionProcessingAttempts { get; set; } + /// + /// Gets or sets the UTC timestamp of the last post-session processing attempt. + /// + public DateTime? PostSessionProcessingLastAttemptUtc { get; set; } + /// + /// Gets or sets whether post-session tasks (custom AI tasks) have been processed. + /// Used to track partial completion so successful steps are not re-run on retry. + /// + public bool IsPostSessionTasksProcessed { get; set; } + /// + /// Gets or sets whether analytics events (resolution detection and session-end metrics) + /// have been recorded. Used to track partial completion so successful steps are not re-run on retry. + /// + public bool IsAnalyticsRecorded { get; set; } + /// + /// Gets or sets whether conversion goals have been evaluated. + /// Tracked independently from analytics so each step can be retried without re-running the other. + /// + public bool IsConversionGoalsEvaluated { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionEntry.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionEntry.cs new file mode 100644 index 00000000..3195f0b5 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionEntry.cs @@ -0,0 +1,24 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// A lightweight representation of an AI chat session used for listing purposes. +/// Contains only the fields needed to display session summaries without loading the full document. +/// +public sealed class AIChatSessionEntry +{ + public string SessionId { get; set; } + + public string ProfileId { get; set; } + + public string Title { get; set; } + + public string UserId { get; set; } + + public string ClientId { get; set; } + + public ChatSessionStatus Status { get; set; } + + public DateTime CreatedUtc { get; set; } + + public DateTime LastActivityUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionEvent.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionEvent.cs new file mode 100644 index 00000000..f205d702 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionEvent.cs @@ -0,0 +1,100 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Tracks a chat session event for analytics purposes. +/// One record is created per chat session to capture usage metrics. +/// +public sealed class AIChatSessionEvent : ExtensibleEntity +{ + /// + /// Gets or sets the unique identifier for the chat session this event is associated with. + /// + public string SessionId { get; set; } + /// + /// Gets or sets the AI profile identifier used in this session. + /// + public string ProfileId { get; set; } + /// + /// Gets or sets the persistent anonymous visitor identifier. + /// Generated on the client side and stored in localStorage for cross-session tracking. + /// + public string VisitorId { get; set; } + /// + /// Gets or sets the authenticated user identifier, if available. + /// + public string UserId { get; set; } + /// + /// Gets or sets whether the user was authenticated during this session. + /// + public bool IsAuthenticated { get; set; } + /// + /// Gets or sets the UTC timestamp when the session started. + /// + public DateTime SessionStartedUtc { get; set; } + /// + /// Gets or sets the UTC timestamp when the session ended. + /// Null if the session is still active. + /// + public DateTime? SessionEndedUtc { get; set; } + /// + /// Gets or sets the total number of messages exchanged in this session (user + assistant). + /// + public int MessageCount { get; set; } + /// + /// Gets or sets the total handle time in seconds (duration from first to last message). + /// + public double HandleTimeSeconds { get; set; } + /// + /// Gets or sets whether the session was resolved within the chat (natural ending) + /// versus abandoned (closed due to inactivity). + /// Used to calculate containment rate. + /// + public bool IsResolved { get; set; } + /// + /// Gets or sets the total number of input tokens consumed across all completions in this session. + /// + public int TotalInputTokens { get; set; } + /// + /// Gets or sets the total number of output tokens generated across all completions in this session. + /// + public int TotalOutputTokens { get; set; } + /// + /// Gets or sets the average AI response latency in milliseconds across all completions in this session. + /// + public double AverageResponseLatencyMs { get; set; } + /// + /// Gets or sets the number of assistant responses that contributed to . + /// + public int CompletionCount { get; set; } + /// + /// Gets or sets the user's feedback rating for this session. + /// Null means no feedback was provided, true means positive (thumbs up), false means negative (thumbs down). + /// + public bool? UserRating { get; set; } + /// + /// Gets or sets the total number of thumbs-up ratings across all messages in this session. + /// + public int ThumbsUpCount { get; set; } + /// + /// Gets or sets the total number of thumbs-down ratings across all messages in this session. + /// + public int ThumbsDownCount { get; set; } + /// + /// Gets or sets the aggregate conversion score across all goals. + /// Null if conversion metrics are not enabled. + /// + public int? ConversionScore { get; set; } + /// + /// Gets or sets the maximum possible conversion score across all goals. + /// Null if conversion metrics are not enabled. + /// + public int? ConversionMaxScore { get; set; } + /// + /// Gets or sets the individual goal results from AI evaluation. + /// + public List ConversionGoalResults { get; set; } = []; + /// + /// Gets or sets the UTC timestamp when this event record was created. + /// + public DateTime CreatedUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionExtractedDataRecord.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionExtractedDataRecord.cs new file mode 100644 index 00000000..f585be8e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionExtractedDataRecord.cs @@ -0,0 +1,43 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Stores the extracted-data snapshot for a single AI chat session so the values +/// can be queried without loading the live chat-session document. +/// +public sealed class AIChatSessionExtractedDataRecord : ExtensibleEntity +{ + /// + /// Gets or sets the record identifier. + /// + public string ItemId { get; set; } + + /// + /// Gets or sets the chat session identifier. + /// + public string SessionId { get; set; } + + /// + /// Gets or sets the AI profile identifier for the session. + /// + public string ProfileId { get; set; } + + /// + /// Gets or sets the UTC time when the session started. + /// + public DateTime SessionStartedUtc { get; set; } + + /// + /// Gets or sets the UTC time when the session ended, when available. + /// + public DateTime? SessionEndedUtc { get; set; } + + /// + /// Gets or sets the extracted values grouped by extraction field name. + /// + public Dictionary> Values { get; set; } = []; + + /// + /// Gets or sets the UTC time when the snapshot record was last updated. + /// + public DateTime UpdatedUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionPrompt.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionPrompt.cs new file mode 100644 index 00000000..8a55ff02 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionPrompt.cs @@ -0,0 +1,31 @@ +using CrestApps.Core.Models; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Models; + +public sealed class AIChatSessionPrompt : CatalogItem +{ + /// + /// Gets or sets the session identifier this prompt belongs to. + /// + public string SessionId { get; set; } + + public ChatRole Role { get; set; } + + public string Content { get; set; } + + public string Title { get; set; } + + public bool IsGeneratedPrompt { get; set; } + + public IEnumerable ContentItemIds { get; set; } + + public bool? UserRating { get; set; } + + public Dictionary References { get; set; } + + /// + /// Gets or sets the UTC date and time when the prompt was created. + /// + public DateTime CreatedUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionQueryContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionQueryContext.cs new file mode 100644 index 00000000..a91c2a84 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionQueryContext.cs @@ -0,0 +1,10 @@ +namespace CrestApps.Core.AI.Models; + +public sealed class AIChatSessionQueryContext +{ + public string ProfileId { get; set; } + + public string Name { get; set; } + + public bool Sorted { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionResult.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionResult.cs new file mode 100644 index 00000000..81ecb027 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIChatSessionResult.cs @@ -0,0 +1,8 @@ +namespace CrestApps.Core.AI.Models; + +public class AIChatSessionResult +{ + public int Count { get; set; } + + public IEnumerable Sessions { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContext.cs new file mode 100644 index 00000000..de471f0a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContext.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; + +namespace CrestApps.Core.AI.Models; + +public class AICompletionContext +{ + public string ConnectionName { get; set; } + + public bool DisableTools { get; set; } + + public string SystemMessage { get; set; } + + public float? Temperature { get; set; } + + public float? TopP { get; set; } + + public float? FrequencyPenalty { get; set; } + + public float? PresencePenalty { get; set; } + + public int? MaxTokens { get; set; } + + public int? PastMessagesCount { get; set; } + + public bool UseCaching { get; set; } = true; + + public string[] ToolNames { get; set; } + + public string[] AgentNames { get; set; } + + public string[] McpConnectionIds { get; set; } + + public string[] A2AConnectionIds { get; set; } + + public string DataSourceId { get; set; } + + public string ChatDeploymentName { get; set; } + + public string UtilityDeploymentName { get; set; } + + [JsonInclude] + [JsonPropertyName("DeploymentId")] + private string _deploymentIdBackingField + { + set => ChatDeploymentName = value; + } + + [JsonInclude] + [JsonPropertyName("ChatDeploymentId")] + private string _chatDeploymentIdBackingField + { + set => ChatDeploymentName = value; + } + + [JsonInclude] + [JsonPropertyName("UtilityDeploymentId")] + private string _utilityDeploymentIdBackingField + { + set => UtilityDeploymentName = value; + } + + public Dictionary AdditionalProperties { get; } = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContextBuilderEvents.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContextBuilderEvents.cs new file mode 100644 index 00000000..43909a0d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContextBuilderEvents.cs @@ -0,0 +1,40 @@ +using CrestApps.Core.AI.Completions; + +namespace CrestApps.Core.AI.Models; + +/// +/// Carries state while an is being constructed. +/// +/// +/// This context is provided to BuildingAsync handlers in the +/// pipeline, allowing implementations to enrich, +/// validate, or otherwise modify the mutable based on the source +/// . +/// +public sealed class AICompletionContextBuildingContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The source resource driving the build. + /// The mutable being built. + /// Thrown when or is . + public AICompletionContextBuildingContext(object resource, AICompletionContext context) + { + ArgumentNullException.ThrowIfNull(resource); + ArgumentNullException.ThrowIfNull(context); + + Resource = resource; + Context = context; + } + + /// + /// Gets the source resource used to seed and configure the completion context. + /// + public object Resource { get; } + + /// + /// Gets the mutable being built. Handlers may mutate this instance. + /// + public AICompletionContext Context { get; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContextBuiltContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContextBuiltContext.cs new file mode 100644 index 00000000..3c026db7 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionContextBuiltContext.cs @@ -0,0 +1,40 @@ +using CrestApps.Core.AI.Completions; + +namespace CrestApps.Core.AI.Models; + +/// +/// Carries state after an has been constructed. +/// +/// +/// This context is provided to BuiltAsync handlers in the +/// pipeline. At this stage, the +/// reflects all handler mutations performed during BuildingAsync, and any caller-supplied +/// configuration delegate. +/// +public sealed class AICompletionContextBuiltContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The source resource used to build the context. + /// The finalized . + /// Thrown when or is . + public AICompletionContextBuiltContext(object resource, AICompletionContext context) + { + ArgumentNullException.ThrowIfNull(resource); + ArgumentNullException.ThrowIfNull(context); + + Resource = resource; + Context = context; + } + + /// + /// Gets the source resource associated with the built completion context. + /// + public object Resource { get; } + + /// + /// Gets the finalized . + /// + public AICompletionContext Context { get; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionReference.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionReference.cs new file mode 100644 index 00000000..f33f970f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionReference.cs @@ -0,0 +1,24 @@ +namespace CrestApps.Core.AI.Models; + +public sealed class AICompletionReference +{ + public string Text { get; set; } + + public string Link { get; set; } + + public string Title { get; set; } + + public int Index { get; set; } + + /// + /// Gets or sets the raw reference identifier from the source index. + /// + public string ReferenceId { get; set; } + + /// + /// Gets or sets the type of the reference source + /// (e.g., the source index profile type for data sources, or "Document" for uploaded documents). + /// Used to determine how links should be generated. + /// + public string ReferenceType { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionUsageRecord.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionUsageRecord.cs new file mode 100644 index 00000000..d1e3c9f2 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AICompletionUsageRecord.cs @@ -0,0 +1,46 @@ +namespace CrestApps.Core.AI.Models; + +public sealed class AICompletionUsageRecord : ExtensibleEntity +{ + public string ContextType { get; set; } + + public string SessionId { get; set; } + + public string ProfileId { get; set; } + + public string InteractionId { get; set; } + + public string UserId { get; set; } + + public string UserName { get; set; } + + public string VisitorId { get; set; } + + public string ClientId { get; set; } + + public bool IsAuthenticated { get; set; } + + public string ProviderName { get; set; } + + public string ClientName { get; set; } + + public string ConnectionName { get; set; } + + public string DeploymentName { get; set; } + + public string ModelName { get; set; } + + public string ResponseId { get; set; } + + public bool IsStreaming { get; set; } + + public int InputTokenCount { get; set; } + + public int OutputTokenCount { get; set; } + + public int TotalTokenCount { get; set; } + + public double ResponseLatencyMs { get; set; } + + public DateTime CreatedUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDataSource.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDataSource.cs new file mode 100644 index 00000000..b78003ab --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDataSource.cs @@ -0,0 +1,69 @@ +using CrestApps.Core.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Models; + +public sealed class AIDataSource : CatalogItem, IDisplayTextAwareModel, ICloneable +{ + [Obsolete("Do no use any more.")] + public string ProfileSource { get; set; } + + [Obsolete("Do no use any more.")] + public string Type { get; set; } + + public string DisplayText { get; set; } + + public DateTime CreatedUtc { get; set; } + + public string Author { get; set; } + + public string OwnerId { get; set; } + + /// + /// Gets or sets the name of the source index to query for data. + /// + public string SourceIndexProfileName { get; set; } + + /// + /// Gets or sets the name of the AI knowledge base index used to store document embeddings. + /// + public string AIKnowledgeBaseIndexProfileName { get; set; } + + /// + /// Gets or sets the source index field name that maps to the document key (reference ID). + /// When not mapped, the document's native key (_id) is used. + /// + public string KeyFieldName { get; set; } + + /// + /// Gets or sets the source index field name that maps to the document title. + /// + public string TitleFieldName { get; set; } + + /// + /// Gets or sets the source index field name that maps to the document content (text). + /// + public string ContentFieldName { get; set; } + + public AIDataSource Clone() + { + return new AIDataSource + { + ItemId = ItemId, + DisplayText = DisplayText, + CreatedUtc = CreatedUtc, +#pragma warning disable CS0618 // Type or member is obsolete + ProfileSource = ProfileSource, + Type = Type, +#pragma warning restore CS0618 // Type or member is obsolete + Author = Author, + OwnerId = OwnerId, + SourceIndexProfileName = SourceIndexProfileName, + AIKnowledgeBaseIndexProfileName = AIKnowledgeBaseIndexProfileName, + KeyFieldName = KeyFieldName, + TitleFieldName = TitleFieldName, + ContentFieldName = ContentFieldName, + Properties = Properties.Clone(), + }; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeployment.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeployment.cs new file mode 100644 index 00000000..3ff3354d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeployment.cs @@ -0,0 +1,71 @@ +using System.Text.Json.Serialization; +using CrestApps.Core.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Models; +public class AIDeployment : SourceCatalogEntry, INameAwareModel, ISourceAwareModel, ICloneable +{ + private string _modelName; + /// + /// Gets or sets the technical name of the AI client implementation to use for this deployment. + /// This maps to a registered key in AIOptions.Clients. + /// For connection-based deployments, this is typically derived from the connection's ClientName. + /// + public string ClientName { get => Source; set => Source = value; } + + [Obsolete("Use ClientName instead. Retained for backward compatibility.")] + [JsonIgnore] + public string ProviderName { get => Source; set => Source = value; } + + [JsonInclude] + [JsonPropertyName("ProviderName")] + private string _providerNameBackingField { set => Source = value; } + /// + /// Gets or sets the unique technical name used to identify this deployment in settings, profiles, and recipes. + /// + public string Name { get; set; } + /// + /// Gets or sets the provider-facing model or deployment name. + /// Falls back to for backward compatibility with legacy records. + /// + public string ModelName { get => string.IsNullOrWhiteSpace(_modelName) ? Name : _modelName; set => _modelName = value?.Trim(); } + public string ConnectionName { get; set; } + public string ConnectionNameAlias { get; set; } + /// + /// Gets or sets the capability types of this deployment (Chat, Utility, Embedding, Image, SpeechToText, TextToSpeech). + /// A deployment can support one or more capabilities. + /// + public AIDeploymentType Type { get; set; } + /// + /// Gets or sets whether this deployment is the default for its selected capability types + /// within its connection. + /// + public bool IsDefault { get; set; } + public DateTime CreatedUtc { get; set; } + public string Author { get; set; } + public string OwnerId { get; set; } + + public bool SupportsType(AIDeploymentType type) + { + return Type.Supports(type); + } + + public AIDeployment Clone() + { + return new AIDeployment + { + ItemId = ItemId, + Name = Name, + ModelName = _modelName, + Source = Source, + ConnectionName = ConnectionName, + ConnectionNameAlias = ConnectionNameAlias, + Type = Type, + IsDefault = IsDefault, + CreatedUtc = CreatedUtc, + Author = Author, + OwnerId = OwnerId, + Properties = Properties.Clone(), + }; + } +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeploymentType.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeploymentType.cs new file mode 100644 index 00000000..ac1a79cd --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeploymentType.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core.AI.Models; + +[Flags] +public enum AIDeploymentType +{ + None = 0, + Chat = 1 << 0, + Utility = 1 << 1, + Embedding = 1 << 2, + Image = 1 << 3, + SpeechToText = 1 << 4, + TextToSpeech = 1 << 5, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeploymentTypeExtensions.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeploymentTypeExtensions.cs new file mode 100644 index 00000000..70c70dfd --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDeploymentTypeExtensions.cs @@ -0,0 +1,19 @@ +namespace CrestApps.Core.AI.Models; +public static class AIDeploymentTypeExtensions +{ + private static readonly AIDeploymentType _allSupportedTypes = Enum.GetValues().Where(type => type != AIDeploymentType.None).Aggregate(AIDeploymentType.None, static (current, type) => current | type); + public static bool Supports(this AIDeploymentType value, AIDeploymentType type) + { + return type != AIDeploymentType.None && (value & type) == type; + } + + public static bool IsValidSelection(this AIDeploymentType value) + { + return value != AIDeploymentType.None && (value & ~_allSupportedTypes) == 0; + } + + public static IEnumerable GetSupportedTypes(this AIDeploymentType value) + { + return Enum.GetValues().Where(type => value.Supports(type)); + } +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocument.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocument.cs new file mode 100644 index 00000000..1c24202a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocument.cs @@ -0,0 +1,40 @@ +using CrestApps.Core.Models; + +namespace CrestApps.Core.AI.Models; + +/// +/// Represents an attached document for AI functionality. +/// Used for RAG (Retrieval Augmented Generation) for both AI Profiles and Chat Interactions. +/// +public sealed class AIDocument : CatalogItem +{ + /// + /// Gets or sets the identifier of the owning resource (e.g., AI Profile ID or Chat Interaction ID). + /// + public string ReferenceId { get; set; } + + /// + /// Gets or sets the type of the owning resource (e.g., "profile" or "chatinteraction"). + /// + public string ReferenceType { get; set; } + + /// + /// Gets or sets the original file name of the document. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the content type (MIME type) of the document. + /// + public string ContentType { get; set; } + + /// + /// Gets or sets the size of the original file in bytes. + /// + public long FileSize { get; set; } + + /// + /// Gets or sets the UTC date and time when the document was uploaded. + /// + public DateTime UploadedUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocumentChunk.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocumentChunk.cs new file mode 100644 index 00000000..6eddc836 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocumentChunk.cs @@ -0,0 +1,45 @@ +using CrestApps.Core.Models; + +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a single chunk of text extracted from an . +/// Stored as a separate record to avoid bloating the parent +/// with large embedding arrays. +/// +public sealed class AIDocumentChunk : CatalogItem +{ + /// + /// Gets or sets the identifier of the parent . + /// + public string AIDocumentId { get; set; } + + /// + /// Gets or sets the identifier of the owning resource (e.g., AI Profile ID or Chat Interaction ID). + /// Denormalized from the parent document for efficient query access. + /// + public string ReferenceId { get; set; } + + /// + /// Gets or sets the type of the owning resource (e.g., "profile" or "chatinteraction"). + /// Denormalized from the parent document for efficient query access. + /// + public string ReferenceType { get; set; } + + /// + /// Gets or sets the text content of this chunk. + /// + public string Content { get; set; } + + /// + /// Gets or sets the embedding vector for this chunk. + /// Stored alongside the content to avoid regenerating embeddings + /// when the vector index is rebuilt or re-indexed. + /// + public float[] Embedding { get; set; } + + /// + /// Gets or sets the chunk index within the parent document. + /// + public int Index { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocumentChunkContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocumentChunkContext.cs new file mode 100644 index 00000000..8494cde7 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIDocumentChunkContext.cs @@ -0,0 +1,49 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a single chunk of an AI document passed to the vector indexing pipeline. +/// This model is used as the record in +/// when indexing document chunks via . +/// +public sealed class AIDocumentChunkContext +{ + /// + /// Gets or sets the unique identifier for this chunk (format: "{documentId}_{chunkIndex}"). + /// + public string ChunkId { get; set; } + + /// + /// Gets or sets the identifier of the parent document. + /// + public string DocumentId { get; set; } + + /// + /// Gets or sets the text content of this chunk. + /// + public string Content { get; set; } + + /// + /// Gets or sets the original file name of the parent document. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the identifier of the owning resource (e.g., AI Profile ID or Chat Interaction ID). + /// + public string ReferenceId { get; set; } + + /// + /// Gets or sets the type of the owning resource (e.g., "profile" or "chatinteraction"). + /// + public string ReferenceType { get; set; } + + /// + /// Gets or sets the chunk index within the parent document. + /// + public int ChunkIndex { get; set; } + + /// + /// Gets or sets the embedding vector for this chunk. + /// + public float[] Embedding { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIMemoryEntry.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIMemoryEntry.cs new file mode 100644 index 00000000..68300813 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIMemoryEntry.cs @@ -0,0 +1,18 @@ +using CrestApps.Core.Models; + +namespace CrestApps.Core.AI.Models; + +public sealed class AIMemoryEntry : CatalogItem +{ + public string UserId { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public string Content { get; set; } + + public DateTime CreatedUtc { get; set; } + + public DateTime UpdatedUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIMemorySearchResult.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIMemorySearchResult.cs new file mode 100644 index 00000000..07f4937e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIMemorySearchResult.cs @@ -0,0 +1,16 @@ +namespace CrestApps.Core.AI.Models; + +public sealed class AIMemorySearchResult +{ + public string MemoryId { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public string Content { get; set; } + + public DateTime? UpdatedUtc { get; set; } + + public float Score { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfile.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfile.cs new file mode 100644 index 00000000..979f64e2 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfile.cs @@ -0,0 +1,189 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using CrestApps.Core.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Models; + +public sealed class AIProfile : SourceCatalogEntry, INameAwareModel, IDisplayTextAwareModel, ICloneable +{ + /// + /// Gets or sets the technical name of the profile. + /// + public string Name { get; set; } + + /// + /// Gets or sets the display text of the profile. + /// + public string DisplayText { get; set; } + + /// + /// Gets or sets the type of AI chat profile. + /// + public AIProfileType Type { get; set; } + + /// + /// Gets or sets a description of the profile's capabilities. + /// Required for profiles, where it describes + /// what the agent can do so the orchestrator can decide when to invoke it. + /// + public string Description { get; set; } + + /// + /// Gets or sets the legacy connection name for the profile. + /// Retained for backward compatibility with older stored profiles. + /// + [Obsolete("Use ChatDeploymentName and UtilityDeploymentName. The selected deployment determines the connection.")] + public string ConnectionName { get; set; } + + /// + /// Gets or sets the chat deployment technical name for this profile. + /// + public string ChatDeploymentName { get; set; } + + /// + /// Gets or sets the utility deployment technical name for this profile. + /// When not set, falls back to the global default utility deployment. + /// + public string UtilityDeploymentName { get; set; } + + [JsonIgnore] + [Obsolete("Use ChatDeploymentName instead. Retained for backward compatibility.")] + public string ChatDeploymentId + { + get => ChatDeploymentName; + set => ChatDeploymentName = value; + } + + [JsonIgnore] + [Obsolete("Use UtilityDeploymentName instead. Retained for backward compatibility.")] + public string UtilityDeploymentId + { + get => UtilityDeploymentName; + set => UtilityDeploymentName = value; + } + + [Obsolete("Use ChatDeploymentName instead. Retained for backward compatibility.")] + [JsonIgnore] + public string DeploymentId + { + get => ChatDeploymentName; + set => ChatDeploymentName = value; + } + + [JsonInclude] + [JsonPropertyName("DeploymentId")] + private string _deploymentIdBackingField + { + set => ChatDeploymentName = value; + } + + [JsonInclude] + [JsonPropertyName("ChatDeploymentId")] + private string _chatDeploymentIdBackingField + { + set => ChatDeploymentName = value; + } + + [JsonInclude] + [JsonPropertyName("UtilityDeploymentId")] + private string _utilityDeploymentIdBackingField + { + set => UtilityDeploymentName = value; + } + + /// + /// Gets or sets the type of title used in the session. + /// + public AISessionTitleType? TitleType { get; set; } + + /// + /// Gets or sets the welcome message shown to users. + /// + public string WelcomeMessage { get; set; } + + /// + /// Gets or sets the subject of the prompt. + /// + public string PromptSubject { get; set; } + + /// + /// Gets or sets the template for the prompt. + /// + public string PromptTemplate { get; set; } + + /// + /// Gets or sets the name of the orchestrator to use for this profile. + /// When or empty, the system default orchestrator is used. + /// + public string OrchestratorName { get; set; } + + /// + /// Gets or sets the UTC timestamp when the profile was created. + /// + public DateTime CreatedUtc { get; set; } + + /// + /// Gets or sets the identifier of the owner of this profile. + /// + public string OwnerId { get; set; } + + /// + /// Gets or sets the author of this profile. + /// + public string Author { get; set; } + + /// + /// Gets or sets the JSON-based settings for the profile. + /// + public JsonObject Settings { get; init; } = []; + + public string GetLegacyConnectionName() + { +#pragma warning disable CS0618 // Type or member is obsolete + return ConnectionName; +#pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Creates a deep copy of the current profile. + /// + /// A cloned instance of . + public AIProfile Clone() + { + return new AIProfile() + { + ItemId = ItemId, + Name = Name, + Source = Source, + DisplayText = DisplayText, + Type = Type, + Description = Description, + OrchestratorName = OrchestratorName, +#pragma warning disable CS0618 // Type or member is obsolete + ConnectionName = ConnectionName, +#pragma warning restore CS0618 // Type or member is obsolete + ChatDeploymentName = ChatDeploymentName, + UtilityDeploymentName = UtilityDeploymentName, + TitleType = TitleType, + WelcomeMessage = WelcomeMessage, + PromptSubject = PromptSubject, + PromptTemplate = PromptTemplate, + CreatedUtc = CreatedUtc, + OwnerId = OwnerId, + Author = Author, + Properties = Properties?.Clone(), + Settings = Settings?.Clone(), + }; + } + + public override string ToString() + { + if (string.IsNullOrEmpty(DisplayText)) + { + return Name; + } + + return DisplayText; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileContextBase.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileContextBase.cs new file mode 100644 index 00000000..4ad75298 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileContextBase.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core.AI.Models; + +public abstract class AIProfileContextBase +{ + public AIProfile Profile { get; } + + public AIProfileContextBase(AIProfile profile) + { + ArgumentNullException.ThrowIfNull(profile); + + Profile = profile; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileDataExtractionSettings.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileDataExtractionSettings.cs new file mode 100644 index 00000000..ebc601b8 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileDataExtractionSettings.cs @@ -0,0 +1,26 @@ +namespace CrestApps.Core.AI.Models; + +public class AIProfileDataExtractionSettings +{ + /// + /// Gets or sets whether data extraction is enabled for this profile. + /// + public bool EnableDataExtraction { get; set; } + + /// + /// Gets or sets the interval at which extraction is performed. + /// A value of 1 means every message, 2 means every other message, etc. + /// + public int ExtractionCheckInterval { get; set; } = 1; + + /// + /// Gets or sets the session inactivity timeout in minutes. + /// Sessions inactive longer than this duration will be closed by the background task. + /// + public int SessionInactivityTimeoutInMinutes { get; set; } = 30; + + /// + /// Gets or sets the list of data extraction entries for this profile. + /// + public List DataExtractionEntries { get; set; } = []; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileExtensions.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileExtensions.cs new file mode 100644 index 00000000..af084435 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileExtensions.cs @@ -0,0 +1,102 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace CrestApps.Core.AI.Models; + +/// +/// Extension methods for managing settings in AIProfile. +/// +public static class AIProfileExtensions +{ + private static readonly JsonSerializerOptions _ignoreDefaultValuesSerializer = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + PropertyNameCaseInsensitive = true, + }; + + /// + /// Retrieves settings of type from the profile. + /// If the settings do not exist, a new instance of is returned. + /// + public static T GetSettings(this AIProfile profile) + where T : new() + { + if (profile.Settings == null) + { + return new T(); + } + + var node = profile.Settings[typeof(T).Name]; + + if (node == null) + { + return new T(); + } + + return node.Deserialize(_ignoreDefaultValuesSerializer) ?? new T(); + } + + /// + /// Attempts to retrieve settings of type from the profile. + /// + public static bool TryGetSettings(this AIProfile profile, out T settings) + where T : class + { + if (profile.Settings == null) + { + settings = null; + return false; + } + + var node = profile.Settings[typeof(T).Name]; + + if (node == null) + { + settings = null; + return false; + } + + settings = node.Deserialize(_ignoreDefaultValuesSerializer); + + return true; + } + + /// + /// Alters existing settings or adds new settings of type if one does not exists. + /// + public static AIProfile AlterSettings(this AIProfile profile, Action setting) + where T : class, new() + { + var existingJObject = profile.Settings[typeof(T).Name] as JsonObject; + + if (existingJObject == null) + { + existingJObject = JsonExtensions.FromObject(new T(), _ignoreDefaultValuesSerializer); + profile.Settings[typeof(T).Name] = existingJObject; + } + + var settingsToMerge = existingJObject.Deserialize(_ignoreDefaultValuesSerializer); + + setting(settingsToMerge); + + profile.Settings[typeof(T).Name] = JsonExtensions.FromObject(settingsToMerge, _ignoreDefaultValuesSerializer); + + return profile; + } + + /// + /// Sets or replaces the settings of type in the profile. + /// + public static AIProfile WithSettings(this AIProfile profile, T settings) + { + ArgumentNullException.ThrowIfNull(settings); + + var jObject = JsonExtensions.FromObject(settings, _ignoreDefaultValuesSerializer); + + profile.Settings[typeof(T).Name] = jObject; + + return profile; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfilePostSessionSettings.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfilePostSessionSettings.cs new file mode 100644 index 00000000..5a65910e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfilePostSessionSettings.cs @@ -0,0 +1,25 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Settings for post-session close processing, stored on . +/// When enabled, the system runs AI-powered analysis on the complete conversation +/// transcript after a session is closed. +/// +public class AIProfilePostSessionSettings +{ + /// + /// Gets or sets whether post-session processing is enabled for this profile. + /// + public bool EnablePostSessionProcessing { get; set; } + + /// + /// Gets or sets the list of post-session processing tasks to execute when a session closes. + /// + public List PostSessionTasks { get; set; } = []; + + /// + /// Gets or sets the tool names to make available during post-session processing. + /// When tools are configured, the AI model can invoke them during post-session analysis. + /// + public string[] ToolNames { get; set; } = []; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileQueryContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileQueryContext.cs new file mode 100644 index 00000000..8c38970e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileQueryContext.cs @@ -0,0 +1,8 @@ +using CrestApps.Core.Models; + +namespace CrestApps.Core.AI.Models; + +public sealed class AIProfileQueryContext : QueryContext +{ + public bool IsListableOnly { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileSettings.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileSettings.cs new file mode 100644 index 00000000..f6c26b0d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileSettings.cs @@ -0,0 +1,19 @@ +namespace CrestApps.Core.AI.Models; + +public class AIProfileSettings +{ + /// + /// Gets or sets a value indicating whether the system message is locked to prevent the user from changing it. + /// + public bool LockSystemMessage { get; set; } + + /// + /// Gets or sets a value indicating whether the profile is listable on the UI. + /// + public bool IsListable { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the profile is removable. + /// + public bool IsRemovable { get; set; } = true; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileTemplate.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileTemplate.cs new file mode 100644 index 00000000..65d4e172 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileTemplate.cs @@ -0,0 +1,85 @@ +using CrestApps.Core.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a reusable template. The template holds only generic metadata; +/// source-specific data is stored in +/// via metadata classes such as or +/// . +/// +public sealed class AIProfileTemplate : SourceCatalogEntry, INameAwareModel, IDisplayTextAwareModel, ICloneable +{ + /// + /// Gets or sets the technical name of the template. + /// + public string Name { get; set; } + + /// + /// Gets or sets the display text of the template. + /// + public string DisplayText { get; set; } + + /// + /// Gets or sets the description of what this template provides. + /// + public string Description { get; set; } + + /// + /// Gets or sets the category for grouping templates in the UI. + /// + public string Category { get; set; } + + /// + /// Gets or sets whether this template appears in listing UIs. + /// Defaults to . + /// + public bool IsListable { get; set; } = true; + + /// + /// Gets or sets the UTC timestamp when the template was created. + /// + public DateTime CreatedUtc { get; set; } + + /// + /// Gets or sets the identifier of the owner of this template. + /// + public string OwnerId { get; set; } + + /// + /// Gets or sets the author of this template. + /// + public string Author { get; set; } + + /// + /// Creates a deep copy of the current template. + /// + public AIProfileTemplate Clone() + { + return new AIProfileTemplate + { + ItemId = ItemId, + Source = Source, + Name = Name, + DisplayText = DisplayText, + Description = Description, + Category = Category, + IsListable = IsListable, + CreatedUtc = CreatedUtc, + OwnerId = OwnerId, + Author = Author, + Properties = new Dictionary(Properties), + }; + } + + public override string ToString() + { + if (string.IsNullOrEmpty(DisplayText)) + { + return Name; + } + + return DisplayText; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileType.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileType.cs new file mode 100644 index 00000000..cb0c8c4e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProfileType.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.AI.Models; + +public enum AIProfileType +{ + Chat, + Utility, + TemplatePrompt, + Agent, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProviderConnection.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProviderConnection.cs new file mode 100644 index 00000000..6c2e8b33 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProviderConnection.cs @@ -0,0 +1,104 @@ +using System.Text.Json.Serialization; +using CrestApps.Core.AI.Json; +using CrestApps.Core.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Models; + +[JsonConverter(typeof(AIProviderConnectionJsonConverter))] +public sealed class AIProviderConnection : SourceCatalogEntry, INameAwareModel, IDisplayTextAwareModel, ICloneable +{ + public string Name { get; set; } + + public string DisplayText { get; set; } + + [Obsolete("Use typed AIDeployment records instead. This property is retained for backward compatibility and migration.")] + public string ChatDeploymentName { get; set; } + + [Obsolete("Use typed AIDeployment records instead. This property is retained for backward compatibility and migration.")] + public string EmbeddingDeploymentName { get; set; } + + [Obsolete("Use typed AIDeployment records instead. This property is retained for backward compatibility and migration.")] + public string ImagesDeploymentName { get; set; } + + [Obsolete("Use typed AIDeployment records instead. This property is retained for backward compatibility and migration.")] + public string UtilityDeploymentName { get; set; } + + [Obsolete("Use typed AIDeployment records instead. This property is retained for backward compatibility and migration.")] + public string SpeechToTextDeploymentName { get; set; } + + /// + /// Gets or sets the technical name of the AI client implementation associated with this connection. + /// This maps to a registered key in AIOptions.Clients. + /// + [JsonIgnore] + public string ClientName + { + get => Source; + set => Source = value; + } + + [Obsolete("Use ClientName instead. Retained for backward compatibility.")] + [JsonIgnore] + public string ProviderName + { + get => Source; + set => Source = value; + } + + public DateTime CreatedUtc { get; set; } + + public string Author { get; set; } + + public string OwnerId { get; set; } + + public string GetLegacyChatDeploymentName() + { +#pragma warning disable CS0618 // Type or member is obsolete + return ChatDeploymentName; +#pragma warning restore CS0618 // Type or member is obsolete + } + + public string GetLegacyEmbeddingDeploymentName() + { +#pragma warning disable CS0618 // Type or member is obsolete + return EmbeddingDeploymentName; +#pragma warning restore CS0618 // Type or member is obsolete + } + + public string GetLegacyImageDeploymentName() + { +#pragma warning disable CS0618 // Type or member is obsolete + return ImagesDeploymentName; +#pragma warning restore CS0618 // Type or member is obsolete + } + + public string GetLegacyUtilityDeploymentName() + { +#pragma warning disable CS0618 // Type or member is obsolete + return UtilityDeploymentName; +#pragma warning restore CS0618 // Type or member is obsolete + } + + public AIProviderConnection Clone() + { + return new AIProviderConnection + { + ItemId = ItemId, + Source = Source, + Name = Name, + DisplayText = DisplayText, +#pragma warning disable CS0618 // Type or member is obsolete + ChatDeploymentName = ChatDeploymentName, + EmbeddingDeploymentName = EmbeddingDeploymentName, + ImagesDeploymentName = ImagesDeploymentName, + UtilityDeploymentName = UtilityDeploymentName, + SpeechToTextDeploymentName = SpeechToTextDeploymentName, +#pragma warning restore CS0618 // Type or member is obsolete + CreatedUtc = CreatedUtc, + Author = Author, + OwnerId = OwnerId, + Properties = Properties.Clone(), + }; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProviderOptions.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProviderOptions.cs new file mode 100644 index 00000000..1acb1180 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AIProviderOptions.cs @@ -0,0 +1,41 @@ +using System.Collections.ObjectModel; +using System.Text.Json.Serialization; +using CrestApps.Core.AI.Json; + +namespace CrestApps.Core.AI.Models; + +public class AIProviderOptions +{ + public Dictionary Providers { get; } = new(StringComparer.OrdinalIgnoreCase); +} + +public sealed class AIProvider +{ + [Obsolete("Use typed AIDeployment records with IsDefault instead. Retained for backward compatibility.")] + public string DefaultChatDeploymentName { get; set; } + + [Obsolete("Use typed AIDeployment records with IsDefault instead. Retained for backward compatibility.")] + public string DefaultEmbeddingDeploymentName { get; set; } + + [Obsolete("Use typed AIDeployment records with IsDefault instead. Retained for backward compatibility.")] + public string DefaultImagesDeploymentName { get; set; } + + [Obsolete("Use typed AIDeployment records with IsDefault instead. Retained for backward compatibility.")] + public string DefaultUtilityDeploymentName { get; set; } + + public IDictionary Connections { get; set; } +} + +[JsonConverter(typeof(AIProviderConnectionConverter))] +public sealed class AIProviderConnectionEntry : ReadOnlyDictionary +{ + public AIProviderConnectionEntry(AIProviderConnectionEntry connection) + : base(connection) + { + } + + public AIProviderConnectionEntry(IDictionary dictionary) + : base(dictionary) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AISessionTitleType.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AISessionTitleType.cs new file mode 100644 index 00000000..1d787dff --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AISessionTitleType.cs @@ -0,0 +1,7 @@ +namespace CrestApps.Core.AI.Models; + +public enum AISessionTitleType +{ + InitialPrompt, + Generated, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AgentAvailability.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AgentAvailability.cs new file mode 100644 index 00000000..6ed47442 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AgentAvailability.cs @@ -0,0 +1,19 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Defines the availability mode for an AI agent. +/// +public enum AgentAvailability +{ + /// + /// The agent is included only when matched by semantic or keyword scoring. + /// This is the default mode and minimizes token usage. + /// + OnDemand, + + /// + /// The agent is always included in every completion request automatically. + /// This mode increases token usage but ensures the agent is always available. + /// + AlwaysAvailable, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AssistantMessageAppearance.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AssistantMessageAppearance.cs new file mode 100644 index 00000000..8cfd7b74 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/AssistantMessageAppearance.cs @@ -0,0 +1,27 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Describes how an assistant message should be presented in the chat UI. +/// +public sealed class AssistantMessageAppearance +{ + /// + /// Gets or sets the assistant role label shown in the chat UI. + /// + public string Label { get; set; } + + /// + /// Gets or sets the Font Awesome icon classes for the assistant message. + /// + public string Icon { get; set; } + + /// + /// Gets or sets the CSS class applied to the assistant role label and icon. + /// + public string CssClass { get; set; } + + /// + /// Gets or sets a value indicating whether the streaming animation should be disabled. + /// + public bool DisableStreamingAnimation { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/CapabilitySummary.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/CapabilitySummary.cs new file mode 100644 index 00000000..4ad115ba --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/CapabilitySummary.cs @@ -0,0 +1,35 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a single capability summary from a pre-intent resolution pass. +/// This is a generic, provider-agnostic representation used to give the intent +/// detector contextual information about available external capabilities. +/// +public sealed class CapabilitySummary +{ + /// + /// Gets or sets the identifier of the source (e.g., MCP connection ID) that owns this capability. + /// + public string SourceId { get; set; } + + /// + /// Gets or sets the display name of the source. + /// + public string SourceDisplayText { get; set; } + + /// + /// Gets or sets the name of the capability. + /// + public string Name { get; set; } + + /// + /// Gets or sets the description of the capability. + /// + public string Description { get; set; } + + /// + /// Gets or sets the semantic similarity score between the user prompt + /// and this capability. Higher values indicate stronger relevance. + /// + public float Score { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatContextType.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatContextType.cs new file mode 100644 index 00000000..52aed390 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatContextType.cs @@ -0,0 +1,19 @@ +using CrestApps.Core.AI.ResponseHandling; + +namespace CrestApps.Core.AI.Models; + +/// +/// Identifies the type of chat context a represents. +/// +public enum ChatContextType +{ + /// + /// The context is for an managed by the AI Chat module. + /// + AIChatSession, + + /// + /// The context is for a managed by the Chat Interactions module. + /// + ChatInteraction, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatDocumentInfo.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatDocumentInfo.cs new file mode 100644 index 00000000..a663837c --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatDocumentInfo.cs @@ -0,0 +1,24 @@ +namespace CrestApps.Core.AI.Models; + +public sealed class ChatDocumentInfo +{ + /// + /// Gets or sets the unique identifier for this document. + /// + public string DocumentId { get; set; } + + /// + /// Gets or sets the original file name of the document. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the content type (MIME type) of the document. + /// + public string ContentType { get; set; } + + /// + /// Gets or sets the size of the original file in bytes. + /// + public long FileSize { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteraction.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteraction.cs new file mode 100644 index 00000000..0eea6486 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteraction.cs @@ -0,0 +1,175 @@ +using System.Text.Json.Serialization; +using CrestApps.Core.AI.ResponseHandling; +using CrestApps.Core.Models; + +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a chat interaction which combines AI profile configuration and chat session state. +/// This enables ad-hoc creation and execution of chat profiles without predefined AI Profiles. +/// +public sealed class ChatInteraction : CatalogItem +{ + /// + /// Gets or sets the title of the chat interaction. + /// + public string Title { get; set; } + + /// + /// Gets or sets the user identifier who owns this interaction. + /// + public string OwnerId { get; set; } + + /// + /// Gets or sets the user identifier who owns this interaction. + /// + public string Author { get; set; } + + /// + /// Gets or sets the chat deployment identifier (AI model) to use. + /// + public string ChatDeploymentName { get; set; } + + /// + /// Gets or sets the utility deployment identifier for this interaction. + /// When not set, falls back to the global default utility deployment. + /// + public string UtilityDeploymentName { get; set; } + + [JsonIgnore] + [Obsolete("Use ChatDeploymentName instead. Retained for backward compatibility.")] + public string ChatDeploymentId + { + get => ChatDeploymentName; + set => ChatDeploymentName = value; + } + + [JsonIgnore] + [Obsolete("Use UtilityDeploymentName instead. Retained for backward compatibility.")] + public string UtilityDeploymentId + { + get => UtilityDeploymentName; + set => UtilityDeploymentName = value; + } + + [Obsolete("Use ChatDeploymentName instead. Retained for backward compatibility.")] + [JsonIgnore] + public string DeploymentId + { + get => ChatDeploymentName; + set => ChatDeploymentName = value; + } + + [JsonInclude] + [JsonPropertyName("DeploymentId")] + private string _deploymentIdBackingField + { + set => ChatDeploymentName = value; + } + + [JsonInclude] + [JsonPropertyName("ChatDeploymentId")] + private string _chatDeploymentIdBackingField + { + set => ChatDeploymentName = value; + } + + [JsonInclude] + [JsonPropertyName("UtilityDeploymentId")] + private string _utilityDeploymentIdBackingField + { + set => UtilityDeploymentName = value; + } + + /// + /// Gets or sets the connection name for the AI provider. + /// + public string ConnectionName { get; set; } + + /// + /// Gets or sets the system message/prompt for the AI. + /// + public string SystemMessage { get; set; } + + /// + /// Gets or sets the temperature parameter for AI responses. + /// + public float? Temperature { get; set; } + + /// + /// Gets or sets the TopP parameter for AI responses. + /// + public float? TopP { get; set; } + + /// + /// Gets or sets the frequency penalty parameter. + /// + public float? FrequencyPenalty { get; set; } + + /// + /// Gets or sets the presence penalty parameter. + /// + public float? PresencePenalty { get; set; } + + /// + /// Gets or sets the maximum number of tokens for AI responses. + /// + public int? MaxTokens { get; set; } + + /// + /// Gets or sets the number of past messages to include in context. + /// + public int? PastMessagesCount { get; set; } + + /// + /// Gets or sets the name of the orchestrator to use for this interaction. + /// When or empty, the system default orchestrator is used. + /// + public string OrchestratorName { get; set; } + + /// + /// Gets or sets the technical name of the currently + /// handling prompts for this interaction. When or empty, the default + /// AI handler is used. This value can be changed mid-conversation (e.g., by an AI + /// function that transfers the chat to a live-agent platform). + /// + public string ResponseHandlerName { get; set; } + + /// + /// Gets or sets the list of AI tool names to use. + /// + public IList ToolNames { get; set; } = []; + + /// + /// Gets or sets the list of agent profile names to include. + /// Agents are AI profiles with type + /// that are dynamically exposed as tools for multi-agent orchestration. + /// + public IList AgentNames { get; set; } = []; + + /// + /// Gets or sets the list of MCP connection IDs to use. + /// + public IList McpConnectionIds { get; set; } = []; + + /// + /// Gets or sets the list of A2A connection IDs to use. + /// + public IList A2AConnectionIds { get; set; } = []; + + /// + /// Gets or sets the collection of attached documents for "chat against own data" functionality. + /// Only applicable when Source is AzureOpenAIOwnData. + /// + public List Documents { get; set; } = []; + + /// + /// Gets or sets the UTC date and time when the interaction was created. + /// + public DateTime CreatedUtc { get; set; } + + /// + /// Gets or sets the last document index. + /// + public int DocumentIndex { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionPrompt.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionPrompt.cs new file mode 100644 index 00000000..1eaeae3a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionPrompt.cs @@ -0,0 +1,47 @@ +using CrestApps.Core.Models; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a prompt/message in a chat interaction stored as a separate document. +/// This allows for better performance with large chat histories by avoiding +/// document contention and reducing the size of the main ChatInteraction document. +/// +public sealed class ChatInteractionPrompt : CatalogItem +{ + /// + /// Gets or sets the ChatInteractionId this prompt belongs to. + /// + public string ChatInteractionId { get; set; } + + /// + /// Gets or sets the role of the message sender (user, assistant, system). + /// + public ChatRole Role { get; set; } + + /// + /// Gets or sets the text content of the prompt/message. + /// + public string Text { get; set; } + + /// + /// Gets or sets the title of the prompt (if any). + /// + public string Title { get; set; } + + /// + /// Gets or sets a value indicating whether this prompt was auto-generated by the system. + /// + public bool IsGeneratedPrompt { get; set; } + + /// + /// Gets or sets the references (citations) in this prompt. + /// + public Dictionary References { get; set; } + + /// + /// Gets or sets the UTC date and time when the prompt was created. + /// + public DateTime CreatedUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionPromptContentMetadata.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionPromptContentMetadata.cs new file mode 100644 index 00000000..f4974033 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionPromptContentMetadata.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Metadata for content item references in a chat interaction prompt. +/// Use to attach this to a . +/// +public sealed class ChatInteractionPromptContentMetadata +{ + /// + /// Gets or sets the content item IDs referenced in this prompt. + /// + public IList ContentItemIds { get; set; } = []; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionQueryContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionQueryContext.cs new file mode 100644 index 00000000..64439c95 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatInteractionQueryContext.cs @@ -0,0 +1,14 @@ +using CrestApps.Core.Models; + +namespace CrestApps.Core.AI.Models; + +/// +/// Context for querying chat interactions. +/// +public class ChatInteractionQueryContext : QueryContext +{ + /// + /// Gets or sets the user ID to filter interactions by. + /// + public string UserId { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatMode.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatMode.cs new file mode 100644 index 00000000..a89ee9d9 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatMode.cs @@ -0,0 +1,29 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Defines the chat input/output mode for an AI chat profile or interaction. +/// Controls whether voice features (microphone, text-to-speech) are available. +/// +public enum ChatMode +{ + /// + /// Standard text-only chat. No voice features are enabled. + /// + TextInput, + + /// + /// Audio input mode. A microphone button is shown so users can + /// dictate their prompts via speech-to-text. The user must still + /// manually send the transcribed message. + /// Requires a default speech-to-text deployment to be configured. + /// + AudioInput, + + /// + /// Full conversation mode with two-way voice interaction. + /// The user speaks, the transcript is sent directly as a prompt, + /// the AI response is spoken back, and recording restarts automatically. + /// Requires both speech-to-text and text-to-speech deployments. + /// + Conversation, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatModeProfileSettings.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatModeProfileSettings.cs new file mode 100644 index 00000000..6b92de7a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatModeProfileSettings.cs @@ -0,0 +1,21 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Settings stored on to control the chat mode +/// and voice features for chat UIs using this profile. +/// +public class ChatModeProfileSettings +{ + /// + /// Gets or sets the chat mode for this profile. + /// Defaults to . + /// + public ChatMode ChatMode { get; set; } + + /// + /// Gets or sets the voice name to use for text-to-speech synthesis + /// when the chat mode is . + /// When null or empty, the provider's default voice is used. + /// + public string VoiceName { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotification.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotification.cs new file mode 100644 index 00000000..8486c83c --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotification.cs @@ -0,0 +1,70 @@ +using CrestApps.Core.AI.Chat; + +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a transient UI notification displayed as a system message in the chat interface. +/// Notifications provide visual feedback to users about system state changes such as +/// typing indicators, agent transfers, or session endings. They are separate from +/// chat history and can be dynamically added, updated, or removed via SignalR. +/// +public sealed class ChatNotification +{ + /// + /// Initializes a new instance of with the specified notification type. + /// + /// The notification type, which serves as both the unique identifier + /// and the CSS styling class. Built-in types are defined in . + /// Custom types are also supported. + /// Thrown when is null or whitespace. + public ChatNotification(string type) + { + ArgumentException.ThrowIfNullOrEmpty(type); + + Type = type; + } + + /// + /// Gets the notification type, which serves as both the unique identifier and the CSS + /// styling class for the notification. Only one notification of a given type can be + /// active at a time — sending a new notification with the same type replaces the + /// existing one. Built-in types are defined in . + /// + public string Type { get; private set; } + + /// + /// Gets or sets the display content of the notification. + /// Supports plain text. For example: "Mike is typing..." or + /// "Transferring you to a live agent. Estimated wait: 2 minutes.". + /// + public string Content { get; set; } + + /// + /// Gets or sets an optional FontAwesome icon class for the notification. + /// For example: "fa-solid fa-spinner fa-spin" or "fa-solid fa-headset". + /// + public string Icon { get; set; } + + /// + /// Gets or sets an optional CSS class to apply to the notification container. + /// Use this for custom visual styling beyond the built-in type-based styles. + /// + public string CssClass { get; set; } + + /// + /// Gets or sets whether the user can dismiss this notification by clicking a close button. + /// + public bool Dismissible { get; set; } + + /// + /// Gets or sets the list of action buttons displayed within the notification. + /// Actions trigger callbacks to the server via HandleNotificationAction. + /// + public IList Actions { get; set; } + + /// + /// Gets or sets an extensible metadata dictionary for passing additional + /// data to the client. For example, estimated wait time or agent name. + /// + public IDictionary Metadata { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotificationAction.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotificationAction.cs new file mode 100644 index 00000000..3287a732 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotificationAction.cs @@ -0,0 +1,39 @@ +using CrestApps.Core.AI.Chat; + +namespace CrestApps.Core.AI.Models; + +/// +/// Represents an action button within a . +/// When clicked, the action triggers a callback to the server via the +/// HandleNotificationAction hub method, which dispatches to +/// registered implementations. +/// +public class ChatNotificationAction +{ + /// + /// Gets or sets the unique action identifier. + /// This value is sent to the server when the user clicks the button + /// and is used to resolve the appropriate . + /// Built-in actions: "cancel-transfer", "end-session". + /// + public string Name { get; set; } + + /// + /// Gets or sets the display label for the action button. + /// For example: "Cancel Transfer" or "End Chat". + /// + public string Label { get; set; } + + /// + /// Gets or sets the CSS class for the action button. + /// For example: "btn-outline-danger" or "btn-outline-secondary". + /// Defaults to "btn-outline-secondary" on the client if not specified. + /// + public string CssClass { get; set; } + + /// + /// Gets or sets an optional FontAwesome icon class for the button. + /// For example: "fa-solid fa-xmark". + /// + public string Icon { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotificationActionContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotificationActionContext.cs new file mode 100644 index 00000000..231cfd92 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatNotificationActionContext.cs @@ -0,0 +1,40 @@ +using CrestApps.Core.AI.Chat; + +namespace CrestApps.Core.AI.Models; + +/// +/// Provides context to an when the user +/// clicks an action button on a chat notification system message. +/// +public sealed class ChatNotificationActionContext +{ + /// + /// Gets the session or interaction identifier. + /// + public required string SessionId { get; init; } + + /// + /// Gets the type of the notification that contains the action. + /// + public required string NotificationType { get; init; } + + /// + /// Gets the name of the action that was triggered. + /// + public required string ActionName { get; init; } + + /// + /// Gets the type of chat context (AI Chat Session or Chat Interaction). + /// + public required ChatContextType ChatType { get; init; } + + /// + /// Gets the SignalR connection ID of the client that triggered the action. + /// + public required string ConnectionId { get; init; } + + /// + /// Gets the scoped service provider for resolving dependencies. + /// + public required IServiceProvider Services { get; init; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatSessionStatus.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatSessionStatus.cs new file mode 100644 index 00000000..c9cef8e8 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ChatSessionStatus.cs @@ -0,0 +1,7 @@ +namespace CrestApps.Core.AI.Models; + +public enum ChatSessionStatus +{ + Active, + Closed, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/CompletionServiceConfigureContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/CompletionServiceConfigureContext.cs new file mode 100644 index 00000000..c7d12d0a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/CompletionServiceConfigureContext.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Models; + +public sealed class CompletionServiceConfigureContext +{ + public string ProviderName { get; set; } + + public string ImplemenationName { get; set; } + + public string DeploymentName { get; set; } + + public bool IsStreaming { get; set; } + + public ChatOptions ChatOptions { get; } + + public readonly AICompletionContext CompletionContext; + + public bool IsFunctionInvocationSupported { get; } + + public Dictionary AdditionalProperties { get; set; } + + public CompletionServiceConfigureContext( + ChatOptions chatOptions, + AICompletionContext completionContext, + bool isFunctionInvocationSupported) + { + ArgumentNullException.ThrowIfNull(chatOptions); + ArgumentNullException.ThrowIfNull(completionContext); + + ChatOptions = chatOptions; + CompletionContext = completionContext; + IsFunctionInvocationSupported = isFunctionInvocationSupported; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ConversionGoal.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ConversionGoal.cs new file mode 100644 index 00000000..1e8a7608 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ConversionGoal.cs @@ -0,0 +1,30 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Defines a single conversion goal used to measure chat session success. +/// Each goal is evaluated by AI after session close and scored within the configured range. +/// +public sealed class ConversionGoal +{ + /// + /// Gets or sets the unique name for this goal. + /// Must be alphanumeric with underscores only. + /// + public string Name { get; set; } + + /// + /// Gets or sets a human-readable description of what constitutes success for this goal. + /// This is provided to the AI model as evaluation criteria. + /// + public string Description { get; set; } + + /// + /// Gets or sets the minimum score for this goal. Defaults to 0. + /// + public int MinScore { get; set; } + + /// + /// Gets or sets the maximum score for this goal. Defaults to 10. + /// + public int MaxScore { get; set; } = 10; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ConversionGoalResult.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ConversionGoalResult.cs new file mode 100644 index 00000000..8151b27b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ConversionGoalResult.cs @@ -0,0 +1,27 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Stores the AI-evaluated result for a single conversion goal. +/// +public sealed class ConversionGoalResult +{ + /// + /// Gets or sets the name of the goal that was evaluated. + /// + public string Name { get; set; } + + /// + /// Gets or sets the AI-assigned score for this goal. + /// + public int Score { get; set; } + + /// + /// Gets or sets the maximum possible score for this goal. + /// + public int MaxScore { get; set; } + + /// + /// Gets or sets an optional AI-generated explanation for the assigned score. + /// + public string Reasoning { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataExtractionEntry.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataExtractionEntry.cs new file mode 100644 index 00000000..5e7b03ff --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataExtractionEntry.cs @@ -0,0 +1,25 @@ +namespace CrestApps.Core.AI.Models; + +public sealed class DataExtractionEntry +{ + /// + /// Gets or sets the unique key for this extraction entry. + /// Must be alphanumeric with underscores only. + /// + public string Name { get; set; } + + /// + /// Gets or sets a description of what to extract. + /// + public string Description { get; set; } + + /// + /// Gets or sets whether to keep extracting values across messages. + /// + public bool AllowMultipleValues { get; set; } + + /// + /// Gets or sets whether to allow replacing a single-value field. + /// + public bool IsUpdatable { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataSourceEmbeddingDocument.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataSourceEmbeddingDocument.cs new file mode 100644 index 00000000..e2ff433a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataSourceEmbeddingDocument.cs @@ -0,0 +1,61 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a single chunk stored in the knowledge base index. +/// Each chunk is an independent document with its own embedding and filter fields. +/// +public sealed class DataSourceEmbeddingDocument +{ + /// + /// Gets or sets the reference ID (key) of the source document this chunk belongs to. + /// + public string ReferenceId { get; set; } + + /// + /// Gets or sets the data source ID this chunk belongs to. + /// + public string DataSourceId { get; set; } + + /// + /// Gets or sets the reference type that identifies the kind of source + /// (e.g., "Content" for Orchard Core content items, or the source index profile type). + /// Used to determine how links for references should be generated. + /// + public string ReferenceType { get; set; } + + /// + /// Gets or sets the unique chunk identifier (e.g., "{referenceId}_{chunkIndex}"). + /// + public string ChunkId { get; set; } + + /// + /// Gets or sets the chunk sequence index within the source document. + /// + public int ChunkIndex { get; set; } + + /// + /// Gets or sets the title of the source document. + /// + public string Title { get; set; } + + /// + /// Gets or sets the text content of this chunk. + /// + public string Content { get; set; } + + /// + /// Gets or sets the embedding vector for this chunk. + /// + public float[] Embedding { get; set; } + + /// + /// Gets or sets the timestamp of the source document. + /// + public DateTime? Timestamp { get; set; } + + /// + /// Gets or sets the filter fields copied from the source document. + /// Keys are prefixed with "filters." (e.g., "filters.status", "filters.category"). + /// + public Dictionary Filters { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataSourceMetadata.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataSourceMetadata.cs new file mode 100644 index 00000000..84788b88 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DataSourceMetadata.cs @@ -0,0 +1,12 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Metadata for storing data source configuration on an entity. +/// +public sealed class DataSourceMetadata +{ + /// + /// Gets or sets the data source ID. + /// + public string DataSourceId { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DocumentIntent.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DocumentIntent.cs new file mode 100644 index 00000000..00a10580 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/DocumentIntent.cs @@ -0,0 +1,25 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Detected intent metadata. +/// +public sealed class DocumentIntent +{ + public required string Name { get; set; } + + public float Confidence { get; set; } + + public string Reason { get; set; } + + public static DocumentIntent FromName(string name, float confidence = 1.0f, string reason = null) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + return new DocumentIntent + { + Name = name, + Confidence = confidence, + Reason = reason, + }; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExportingAIProviderConnectionContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExportingAIProviderConnectionContext.cs new file mode 100644 index 00000000..aecf1326 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExportingAIProviderConnectionContext.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Nodes; + +namespace CrestApps.Core.AI.Models; + +public class ExportingAIProviderConnectionContext +{ + public readonly AIProviderConnection Connection; + + public readonly JsonObject ExportData; + + public ExportingAIProviderConnectionContext(AIProviderConnection connection, JsonObject exportData) + { + ArgumentNullException.ThrowIfNull(connection); + + Connection = connection; + ExportData = exportData ?? []; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayContext.cs new file mode 100644 index 00000000..f16ac27b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayContext.cs @@ -0,0 +1,20 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Provides context for establishing a connection to an external chat relay. +/// Contains the session identity and chat type. +/// +public sealed class ExternalChatRelayContext +{ + /// + /// Gets the session identifier. + /// For , this is . + /// For , this is the ChatInteraction.ItemId. + /// + public required string SessionId { get; init; } + + /// + /// Gets the type of chat context. + /// + public required ChatContextType ChatType { get; init; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayEvent.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayEvent.cs new file mode 100644 index 00000000..72725198 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayEvent.cs @@ -0,0 +1,35 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents an event received from an external chat relay (e.g., a WebSocket connection +/// to a third-party live-agent platform). The event type determines how the framework +/// routes the event to the appropriate notification or message pipeline. +/// +public sealed class ExternalChatRelayEvent +{ + /// + /// Gets or sets the type of the event. + /// Use constants from for well-known types, + /// or any custom string for platform-specific event types. + /// + public string EventType { get; set; } + + /// + /// Gets or sets the text content of the event. + /// For , this is the agent's message text. + /// For , this is the estimated wait time string. + /// For custom event types, this is the event payload. + /// + public string Content { get; set; } + + /// + /// Gets or sets the display name of the agent involved in the event. + /// Used for typing indicators and agent-connected notifications. + /// + public string AgentName { get; set; } + + /// + /// Gets or sets an extensible metadata dictionary for passing additional data. + /// + public IDictionary Metadata { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayEventType.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayEventType.cs new file mode 100644 index 00000000..91a9ca67 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayEventType.cs @@ -0,0 +1,66 @@ +using CrestApps.Core.AI.Chat; + +namespace CrestApps.Core.AI.Models; + +/// +/// Provides well-known event type constants for . +/// These values are used by the default to route +/// events to the appropriate notification or message pipeline. +/// +/// +/// Event types are strings rather than an enum so that third-party integrations can define +/// custom event types without modifying the framework. The default handler ignores any +/// event type it does not recognize and logs a debug message. +/// +public static class ExternalChatRelayEventTypes +{ + /// + /// The external agent is typing a response. + /// + public const string AgentTyping = "agent-typing"; + + /// + /// The external agent has stopped typing. + /// + public const string AgentStoppedTyping = "agent-stopped-typing"; + + /// + /// A live agent has connected to the session. + /// + public const string AgentConnected = "agent-connected"; + + /// + /// A live agent has disconnected from the session. + /// + public const string AgentDisconnected = "agent-disconnected"; + + /// + /// The external agent is reconnecting to the session after a disruption. + /// + public const string AgentReconnecting = "agent-reconnecting"; + + /// + /// The connection to the external system has been lost. + /// + public const string ConnectionLost = "connection-lost"; + + /// + /// The connection to the external system has been restored after a loss. + /// + public const string ConnectionRestored = "connection-restored"; + + /// + /// The external system has sent a chat message. + /// + public const string Message = "message"; + + /// + /// The estimated wait time has been updated. + /// + public const string WaitTimeUpdated = "wait-time-updated"; + + /// + /// The external session has ended. + /// + public const string SessionEnded = "session-ended"; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayNotificationResult.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayNotificationResult.cs new file mode 100644 index 00000000..88d434db --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExternalChatRelayNotificationResult.cs @@ -0,0 +1,32 @@ +using CrestApps.Core.AI.Chat; + +namespace CrestApps.Core.AI.Models; + +/// +/// Describes the notification actions to take when handling an external chat relay event. +/// This is the output from and is processed +/// by . +/// +public sealed class ExternalChatRelayNotificationResult +{ + /// + /// Gets the notification types to remove before sending the new notification. + /// For example, an "agent connected" event might remove the "transfer" notification first. + /// + public IList RemoveNotificationTypes { get; } = []; + + /// + /// Gets or sets the notification to send, or if the event + /// only removes existing notifications (e.g., "agent stopped typing" removes the typing indicator). + /// + public ChatNotification Notification { get; set; } + + /// + /// Gets or sets a value indicating whether the notification should be sent as an update + /// (using ) instead of a new send + /// (using ). + /// When , the notification replaces an existing notification with the same type + /// only if it exists on the client. When (default), the notification is always sent. + /// + public bool IsUpdate { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExtractedFieldState.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExtractedFieldState.cs new file mode 100644 index 00000000..b11e526a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ExtractedFieldState.cs @@ -0,0 +1,14 @@ +namespace CrestApps.Core.AI.Models; + +public sealed class ExtractedFieldState +{ + /// + /// Gets or sets the extracted values. Always a list, even for single-value fields. + /// + public List Values { get; set; } = []; + + /// + /// Gets or sets the UTC timestamp of the last extraction. + /// + public DateTime? LastExtractedUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/IAIProviderConnectionHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/IAIProviderConnectionHandler.cs new file mode 100644 index 00000000..fa23d0ac --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/IAIProviderConnectionHandler.cs @@ -0,0 +1,22 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Handles lifecycle events for AI provider connections, allowing customization +/// of connection initialization and data export behavior. +/// +public interface IAIProviderConnectionHandler +{ + /// + /// Called when an AI provider connection is being initialized, allowing + /// modification of the connection properties before persistence. + /// + /// The context containing the connection being initialized. + void Initializing(InitializingAIProviderConnectionContext context); + + /// + /// Called when an AI provider connection is being exported, allowing + /// sensitive data to be removed or transformed before serialization. + /// + /// The context containing the connection and export data. + void Exporting(ExportingAIProviderConnectionContext context); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/InitializingAIProviderConnectionContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/InitializingAIProviderConnectionContext.cs new file mode 100644 index 00000000..27210cda --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/InitializingAIProviderConnectionContext.cs @@ -0,0 +1,15 @@ +namespace CrestApps.Core.AI.Models; + +public class InitializingAIProviderConnectionContext +{ + public readonly Dictionary Values = []; + + public readonly AIProviderConnection Connection; + + public InitializingAIProviderConnectionContext(AIProviderConnection connection) + { + ArgumentNullException.ThrowIfNull(connection); + + Connection = connection; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/NewAIChatSessionContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/NewAIChatSessionContext.cs new file mode 100644 index 00000000..0d9a6f28 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/NewAIChatSessionContext.cs @@ -0,0 +1,6 @@ +namespace CrestApps.Core.AI.Models; + +public class NewAIChatSessionContext +{ + public bool AllowRobots { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestrationContextBuildingContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestrationContextBuildingContext.cs new file mode 100644 index 00000000..bf5a280f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestrationContextBuildingContext.cs @@ -0,0 +1,40 @@ +using CrestApps.Core.AI.Orchestration; + +namespace CrestApps.Core.AI.Models; + +/// +/// Carries state while an is being constructed. +/// +/// +/// This context is provided to BuildingAsync handlers in the +/// pipeline, allowing implementations to enrich, +/// validate, or otherwise modify the mutable based on the source +/// . +/// +public sealed class OrchestrationContextBuildingContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The source resource driving the build. + /// The mutable being built. + /// Thrown when or is . + public OrchestrationContextBuildingContext(object resource, OrchestrationContext context) + { + ArgumentNullException.ThrowIfNull(resource); + ArgumentNullException.ThrowIfNull(context); + + Resource = resource; + Context = context; + } + + /// + /// Gets the source resource used to seed and configure the orchestration context. + /// + public object Resource { get; } + + /// + /// Gets the mutable being built. Handlers may mutate this instance. + /// + public OrchestrationContext Context { get; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestrationContextBuiltContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestrationContextBuiltContext.cs new file mode 100644 index 00000000..f59ef461 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestrationContextBuiltContext.cs @@ -0,0 +1,40 @@ +using CrestApps.Core.AI.Orchestration; + +namespace CrestApps.Core.AI.Models; + +/// +/// Carries state after an has been constructed. +/// +/// +/// This context is provided to BuiltAsync handlers in the +/// pipeline. At this stage, the +/// reflects all handler mutations performed during BuildingAsync, and any caller-supplied +/// configuration delegate. +/// +public sealed class OrchestrationContextBuiltContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The source resource used to build the context. + /// The finalized . + /// Thrown when or is . + public OrchestrationContextBuiltContext(object resource, OrchestrationContext context) + { + ArgumentNullException.ThrowIfNull(resource); + ArgumentNullException.ThrowIfNull(context); + + Resource = resource; + OrchestrationContext = context; + } + + /// + /// Gets the source resource associated with the built orchestration context. + /// + public object Resource { get; } + + /// + /// Gets the finalized . + /// + public OrchestrationContext OrchestrationContext { get; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestratorAvailability.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestratorAvailability.cs new file mode 100644 index 00000000..78c63743 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestratorAvailability.cs @@ -0,0 +1,17 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents whether an orchestrator is currently available for use. +/// +public sealed class OrchestratorAvailability +{ + /// + /// Gets or sets a value indicating whether the orchestrator is available. + /// + public bool IsAvailable { get; set; } = true; + + /// + /// Gets or sets the message explaining why the orchestrator is unavailable. + /// + public string Message { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestratorContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestratorContext.cs new file mode 100644 index 00000000..4e420079 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/OrchestratorContext.cs @@ -0,0 +1,78 @@ +using System.Text; +using CrestApps.Core.AI.Orchestration; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Models; + +/// +/// Provides the input context for an execution. +/// Contains the user message, conversation history, completion settings, +/// document references, and extensible properties populated by +/// implementations. +/// +/// +/// Instances are created by using a handler pipeline, +/// allowing modules to contribute additional context (e.g., document metadata, MCP connections, +/// data sources) without modifying the core orchestrator. +/// +public sealed class OrchestrationContext +{ + /// + /// Gets or sets the current user message to process. + /// + public string UserMessage { get; set; } + + /// + /// Gets or sets the conversation history (prior messages in the session). + /// + public IList ConversationHistory { get; set; } = []; + + /// + /// Gets or sets the completion context containing tool names, system message, + /// model parameters, and other configuration built from the profile or interaction. + /// + public AICompletionContext CompletionContext { get; set; } + + /// + /// Gets or sets the source/provider name for the completion service + /// (e.g., the AI client implementation name). + /// + public string SourceName { get; set; } + + /// + /// Gets or sets the scoped service provider for this orchestration session. + /// Allows orchestrators to resolve services without constructor injection. + /// + public IServiceProvider ServiceProvider { get; set; } + + /// + /// Gets or sets the document references available for this session. + /// System tools use these to load document content on demand. + /// + public List Documents { get; set; } = []; + + /// + /// Gets or sets whether tools should be disabled for this orchestration. + /// When true, the orchestrator should not inject any tools into the completion context. + /// + public bool DisableTools { get; set; } + + /// + /// Gets the tool names that must be included for the current orchestration request + /// even when relevance scoping is applied. + /// + public List MustIncludeTools { get; } = []; + + /// + /// Gets a shared that orchestration handlers can append to + /// in order to build the final system message. The accumulated content is flushed to + /// after all handlers have run. + /// + public StringBuilder SystemMessageBuilder { get; } = new(); + + /// + /// Gets the extensible property bag for additional context contributed by + /// implementations. + /// + public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionProcessingStatus.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionProcessingStatus.cs new file mode 100644 index 00000000..78afe2c2 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionProcessingStatus.cs @@ -0,0 +1,27 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents the processing status of post-session tasks for a chat session. +/// +public enum PostSessionProcessingStatus +{ + /// + /// No post-session processing is needed or has not been initiated. + /// + None, + + /// + /// Post-session processing is pending and waiting to be executed. + /// + Pending, + + /// + /// Post-session processing completed successfully. + /// + Completed, + + /// + /// Post-session processing failed after exhausting all retry attempts. + /// + Failed, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionResult.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionResult.cs new file mode 100644 index 00000000..807111a0 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionResult.cs @@ -0,0 +1,39 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Stores the result of a single post-session processing task. +/// +public sealed class PostSessionResult +{ + /// + /// Gets or sets the name of the task that produced this result. + /// Matches the from the profile configuration. + /// + public string Name { get; set; } + + /// + /// Gets or sets the AI-generated result value. + /// For Disposition: the selected option. For Summary: the generated text. For Sentiment: Positive/Negative/Neutral. + /// + public string Value { get; set; } + + /// + /// Gets or sets the processing status of this individual task. + /// + public PostSessionTaskResultStatus Status { get; set; } + + /// + /// Gets or sets the error message if the task failed. + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets the number of processing attempts made for this task. + /// + public int Attempts { get; set; } + + /// + /// Gets or sets the UTC timestamp when this result was processed. + /// + public DateTime ProcessedAtUtc { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTask.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTask.cs new file mode 100644 index 00000000..9563a571 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTask.cs @@ -0,0 +1,58 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Defines a single post-session processing task configured on an AI profile. +/// +public sealed class PostSessionTask +{ + /// + /// Gets or sets the unique key for this task. + /// Must be alphanumeric with underscores only. + /// + public string Name { get; set; } + + /// + /// Gets or sets the type of post-session processing to perform. + /// + public PostSessionTaskType Type { get; set; } + + /// + /// Gets or sets the user-provided instructions for the AI model. + /// For , this describes how to select from the options. + /// For , this provides the full processing instructions. + /// + public string Instructions { get; set; } + + /// + /// Gets or sets whether the AI model can select multiple options. + /// Only applicable when is . + /// + public bool AllowMultipleValues { get; set; } + + /// + /// Gets or sets the list of predefined options for this task. + /// Required when is . + /// Each option has a value and an optional description to guide the AI model. + /// + public List Options { get; set; } = []; + + /// + /// Gets or sets the AI tool names available to this task during post-session processing. + /// + public string[] ToolNames { get; set; } = []; + + /// + /// Gets or sets the AI agent profile names available to this task during post-session processing. + /// + public string[] AgentNames { get; set; } = []; + + /// + /// Gets or sets the A2A connection identifiers available to this task during post-session processing. + /// + public string[] A2AConnectionIds { get; set; } = []; + + /// + /// Gets or sets the MCP connection identifiers available to this task during post-session processing. + /// + public string[] McpConnectionIds { get; set; } = []; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskOption.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskOption.cs new file mode 100644 index 00000000..fdeaba07 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskOption.cs @@ -0,0 +1,20 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a single option in a task. +/// The AI model selects from these options when analyzing the conversation. +/// +public sealed class PostSessionTaskOption +{ + /// + /// Gets or sets the value of this option. + /// This is the identifier that will be stored as the result when selected. + /// + public string Value { get; set; } + + /// + /// Gets or sets an optional description that provides additional context + /// to help the AI model understand when to select this option. + /// + public string Description { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskResultStatus.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskResultStatus.cs new file mode 100644 index 00000000..1dd77d2c --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskResultStatus.cs @@ -0,0 +1,22 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents the processing status of an individual post-session task. +/// +public enum PostSessionTaskResultStatus +{ + /// + /// The task has not been attempted yet. + /// + Pending, + + /// + /// The task completed successfully. + /// + Succeeded, + + /// + /// The task failed during execution. + /// + Failed, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskType.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskType.cs new file mode 100644 index 00000000..09ea29d9 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTaskType.cs @@ -0,0 +1,20 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Defines the type of post-session processing task. +/// +public enum PostSessionTaskType +{ + /// + /// The AI selects one or more values from a predefined list of options. + /// Used for dispositions, classifications, or any scenario where the user + /// defines the possible outcomes upfront. + /// + PredefinedOptions, + + /// + /// The AI generates a freeform text value based on the provided instructions. + /// Used for summaries, sentiment analysis, or any open-ended analysis. + /// + Semantic, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PreemptiveRagContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PreemptiveRagContext.cs new file mode 100644 index 00000000..483541a2 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PreemptiveRagContext.cs @@ -0,0 +1,36 @@ +using CrestApps.Core.AI.Memory; + +namespace CrestApps.Core.AI.Models; + +/// +/// Carries the state needed by implementations +/// during a preemptive RAG pass. +/// +public sealed class PreemptiveRagContext +{ + public PreemptiveRagContext(OrchestrationContext orchestrationContext, object resource, IList queries) + { + ArgumentNullException.ThrowIfNull(orchestrationContext); + ArgumentNullException.ThrowIfNull(resource); + ArgumentNullException.ThrowIfNull(queries); + + OrchestrationContext = orchestrationContext; + Resource = resource; + Queries = queries; + } + + /// + /// Gets the current orchestration context, including the system message builder and completion context. + /// + public OrchestrationContext OrchestrationContext { get; } + + /// + /// Gets the source resource (e.g., AIProfile or ChatInteraction). + /// + public object Resource { get; } + + /// + /// Gets the focused search queries extracted by the preemptive search query provider. + /// + public IList Queries { get; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ProfileTemplateMetadata.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ProfileTemplateMetadata.cs new file mode 100644 index 00000000..4f256cdb --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ProfileTemplateMetadata.cs @@ -0,0 +1,164 @@ +using System.Text.Json.Serialization; +using CrestApps.Core.AI.ResponseHandling; + +namespace CrestApps.Core.AI.Models; + +/// +/// Metadata for templates with a "Profile" source. +/// Stored in the template's via +/// Put<ProfileTemplateMetadata> / As<ProfileTemplateMetadata>. +/// +public sealed class ProfileTemplateMetadata +{ + /// + /// Gets or sets the type of AI profile this template creates. + /// + + public AIProfileType? ProfileType { get; set; } + /// + /// Gets or sets the legacy connection name used by older templates. + /// Retained for backward compatibility with existing template metadata. + /// + [Obsolete("Use ChatDeploymentName and UtilityDeploymentName. The selected deployment determines the connection.")] + + public string ConnectionName { get; set; } + /// + /// Gets or sets the chat deployment identifier to pre-fill. + /// + + public string ChatDeploymentName { get; set; } + /// + /// Gets or sets the utility deployment identifier to pre-fill. + /// + + public string UtilityDeploymentName { get; set; } + + [JsonIgnore] + [Obsolete("Use ChatDeploymentName instead. Retained for backward compatibility.")] + public string ChatDeploymentId + { + get => ChatDeploymentName; + set => ChatDeploymentName = value; + + } + + [JsonIgnore] + [Obsolete("Use UtilityDeploymentName instead. Retained for backward compatibility.")] + public string UtilityDeploymentId + { + get => UtilityDeploymentName; + set => UtilityDeploymentName = value; + + } + + [JsonInclude] + [JsonPropertyName("ChatDeploymentId")] + private string _chatDeploymentId + { + set => ChatDeploymentName = value; + + } + + [JsonInclude] + [JsonPropertyName("UtilityDeploymentId")] + private string _utilityDeploymentId + { + set => UtilityDeploymentName = value; + + } + /// + /// Gets or sets the name of the orchestrator to use. + /// + + public string OrchestratorName { get; set; } + /// + /// Gets or sets the system message for the profile. + /// + + public string SystemMessage { get; set; } + /// + /// Gets or sets the welcome message shown to users. + /// + + public string WelcomeMessage { get; set; } + /// + /// Gets or sets the template for the prompt. + /// + + public string PromptTemplate { get; set; } + /// + /// Gets or sets the subject of the prompt. + /// + + public string PromptSubject { get; set; } + /// + /// Gets or sets the type of title used in the session. + /// + + public AISessionTitleType? TitleType { get; set; } + /// + /// Gets or sets the temperature parameter for AI completion. + /// + + public float? Temperature { get; set; } + /// + /// Gets or sets the TopP parameter for AI completion. + /// + + public float? TopP { get; set; } + /// + /// Gets or sets the frequency penalty parameter. + /// + + public float? FrequencyPenalty { get; set; } + /// + /// Gets or sets the presence penalty parameter. + /// + + public float? PresencePenalty { get; set; } + /// + /// Gets or sets the maximum number of tokens for AI completion. + /// + + public int? MaxOutputTokens { get; set; } + /// + /// Gets or sets the number of past messages to include in context. + /// + + public int? PastMessagesCount { get; set; } + /// + /// Gets or sets the tool names to associate with the profile. + /// + + public string[] ToolNames { get; set; } = []; + /// + /// Gets or sets the A2A connection identifiers to associate with the profile. + /// + + public string[] A2AConnectionIds { get; set; } = []; + /// + /// Gets or sets the agent profile names to associate with the profile. + /// + + public string[] AgentNames { get; set; } = []; + /// + /// Gets or sets the description of the profile's capabilities. + /// Used for templates to describe + /// what the agent does. + /// + + public string Description { get; set; } + /// + /// Gets or sets the availability mode for agent profiles. + /// Controls whether the agent is always included in every request + /// or only when matched by relevance scoring. + /// + + public AgentAvailability? AgentAvailability { get; set; } + /// + /// Gets or sets the name of the initial + /// for new sessions created from profiles based on this template. + /// When or empty, the default AI handler is used. + /// + public string InitialResponseHandlerName { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ReceivedMessageContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ReceivedMessageContext.cs new file mode 100644 index 00000000..95feeb01 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ReceivedMessageContext.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Models; + +public sealed class ReceivedMessageContext +{ + public ReceivedMessageContext(ChatResponse completion) + { + ArgumentNullException.ThrowIfNull(completion); + + Completion = completion; + } + + public ChatResponse Completion { get; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ReceivedUpdateContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ReceivedUpdateContext.cs new file mode 100644 index 00000000..63700357 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ReceivedUpdateContext.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Models; + +public sealed class ReceivedUpdateContext +{ + public ReceivedUpdateContext(ChatResponseUpdate update) + { + ArgumentNullException.ThrowIfNull(update); + + Update = update; + } + + public ChatResponseUpdate Update { get; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SpeechVoice.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SpeechVoice.cs new file mode 100644 index 00000000..b2a0c11f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SpeechVoice.cs @@ -0,0 +1,32 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Represents an available voice for text-to-speech synthesis. +/// +public sealed class SpeechVoice +{ + /// + /// Gets or sets the unique identifier of the voice (e.g., "en-US-JennyNeural"). + /// + public string Id { get; set; } + + /// + /// Gets or sets the display name of the voice (e.g., "Jenny"). + /// + public string Name { get; set; } + + /// + /// Gets or sets the language/locale of this voice (e.g., "en-US"). + /// + public string Language { get; set; } + + /// + /// Gets or sets the URL to a sample audio clip of this voice. + /// + public string VoiceSampleUrl { get; set; } + + /// + /// Gets or sets the gender of this voice. + /// + public SpeechVoiceGender Gender { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SpeechVoiceGender.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SpeechVoiceGender.cs new file mode 100644 index 00000000..5159fbf5 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SpeechVoiceGender.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.AI.Models; + +public enum SpeechVoiceGender +{ + Unknown, + Male, + Female, + Neutral, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SystemPromptTemplateMetadata.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SystemPromptTemplateMetadata.cs new file mode 100644 index 00000000..11e152a4 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/SystemPromptTemplateMetadata.cs @@ -0,0 +1,14 @@ +namespace CrestApps.Core.AI.Models; + +/// +/// Metadata for templates with a "SystemPrompt" source. +/// Stored in the template's via +/// Put<SystemPromptTemplateMetadata> / As<SystemPromptTemplateMetadata>. +/// +public sealed class SystemPromptTemplateMetadata +{ + /// + /// Gets or sets the system prompt content. + /// + public string SystemMessage { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ToolRegistryEntry.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ToolRegistryEntry.cs new file mode 100644 index 00000000..c8926c24 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/ToolRegistryEntry.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Models; + +/// +/// Represents a tool entry in the unified tool registry. +/// Contains searchable metadata used for tool scoping during orchestration. +/// +public sealed class ToolRegistryEntry +{ + /// + /// Gets or sets the unique identifier for this entry. + /// Used for internal retrieval and to avoid name collisions across sources. + /// For local/system tools this equals ; + /// for MCP tools it includes the source identifier (e.g., mcp:{connectionId}:{toolName}). + /// + public string Id { get; set; } + + /// + /// Gets or sets the function name of the tool as presented to the AI model. + /// For local tools, this is the registered tool name. + /// For MCP tools, this is the tool name as reported by the MCP server. + /// + public string Name { get; set; } + + /// + /// Gets or sets a human-readable description of the tool's capabilities. + /// Used for relevance matching during tool scoping. + /// + public string Description { get; set; } + + /// + /// Gets or sets the source type of this tool entry. + /// + public ToolRegistryEntrySource Source { get; set; } + + /// + /// Gets or sets the source identifier. + /// For MCP tools, this is the MCP connection ID. + /// For local tools, this is . + /// + public string SourceId { get; set; } + + /// + /// Gets or sets a factory delegate that creates the actual for this entry. + /// Each provider sets this to its own resolution logic (e.g., DI lookup for local tools, + /// MCP proxy creation for MCP tools). + /// + public Func> CreateAsync { get; set; } +} + +/// +/// Identifies the origin of a tool registry entry. +/// +public enum ToolRegistryEntrySource +{ + /// + /// A locally registered AI tool. + /// + Local, + + /// + /// A tool provided by an MCP server connection. + /// + McpServer, + + /// + /// A system tool automatically included by the orchestrator based on context availability. + /// + System, + + /// + /// An AI agent profile exposed as a tool for multi-agent orchestration. + /// + Agent, + + /// + /// A remote agent accessible via the Agent-to-Agent (A2A) protocol. + /// + A2AAgent, +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/AIInvocationContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/AIInvocationContext.cs new file mode 100644 index 00000000..56f95137 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/AIInvocationContext.cs @@ -0,0 +1,78 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.AI.Tooling; + +namespace CrestApps.Core.AI.Orchestration; +/// +/// Per-invocation context for AI operations, providing isolation between concurrent +/// SignalR hub method calls. Each hub invocation creates its own instance via +/// , and AI tools retrieve it via +/// . +/// +/// +/// Why this exists: In SignalR, HttpContext is shared across all hub +/// method invocations on the same WebSocket connection. If a user sends multiple +/// messages concurrently, writing to HttpContext.Items causes data leaks +/// between invocations. This class uses +/// (via ) to provide truly invocation-scoped storage +/// that flows correctly through async/await chains without any cross-invocation +/// contamination. +/// +/// +/// +/// For AI tools: Tools are registered as singletons and the AI model does not +/// pass any invocation identifier when calling them. Tools retrieve the current +/// invocation context by calling AIInvocationScope.Current, which returns the +/// context for the async execution flow that is calling the tool — even when multiple +/// invocations are in flight simultaneously on different threads or continuations. +/// +/// +public sealed class AIInvocationContext +{ + private int _referenceIndex; + /// + /// Gets or sets the for the current invocation, + /// providing provider, connection, and resource information. + /// + public AIToolExecutionContext ToolExecutionContext { get; set; } + /// + /// Gets or sets the completion context for the current invocation. + /// + public AICompletionContext CompletionContext { get; set; } + /// + /// Gets or sets the chat session for the current invocation. + /// + public AIChatSession ChatSession { get; set; } + /// + /// Gets or sets the chat interaction for the current invocation. + /// + public ChatInteraction ChatInteraction { get; set; } + /// + /// Gets or sets the data source identifier for the current invocation. + /// Used by DataSourceSearchTool to scope searches to the correct data source. + /// + public string DataSourceId { get; set; } + /// + /// Gets the dictionary of citation references collected during tool execution + /// (e.g., from DataSourceSearchTool and SearchDocumentsTool). + /// Keyed by the citation marker (e.g., "[doc:1]") with the reference metadata as value. + /// + public Dictionary ToolReferences { get; } = new(StringComparer.OrdinalIgnoreCase); + /// + /// Gets a general-purpose property bag for extensibility. + /// Handlers and tools can store arbitrary per-invocation data here. + /// + public Dictionary Items { get; } = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Returns the next unique reference index for citation markers (e.g., [doc:1], [doc:2], ...). + /// This method is thread-safe and ensures a monotonically increasing counter across all + /// handlers and tools within the same invocation, preventing index collisions between + /// DataSourcePreemptiveRagHandler, DocumentPreemptiveRagHandler, + /// DataSourceSearchTool, and SearchDocumentsTool. + /// + /// The next 1-based reference index. + public int NextReferenceIndex() + { + return Interlocked.Increment(ref _referenceIndex); + } +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/AIInvocationScope.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/AIInvocationScope.cs new file mode 100644 index 00000000..10b5a5b6 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/AIInvocationScope.cs @@ -0,0 +1,84 @@ +namespace CrestApps.Core.AI.Orchestration; +/// +/// Manages an -backed ambient context for AI invocations. +/// Each SignalR hub method call (or any entry point) creates a scope with +/// , and any code running in that async flow can access the +/// context via . +/// +/// +/// Usage in hubs: +/// +/// using var scope = AIInvocationScope.Begin(); +/// // scope.Context is the AIInvocationContext for this invocation. +/// // All downstream code (handlers, tools) can call AIInvocationScope.Current. +/// +/// +/// +/// +/// Usage in AI tools (singletons): +/// +/// var context = AIInvocationScope.Current; +/// var dataSourceId = context?.DataSourceId; +/// +/// Because flows through async/await continuations, +/// each concurrent invocation sees its own context — even when multiple invocations +/// share the same HttpContext in a SignalR WebSocket connection. +/// +/// +public static class AIInvocationScope +{ + private static readonly AsyncLocal _current = new(); + /// + /// Gets the for the current async execution flow, + /// or null if no scope has been started. + /// + public static AIInvocationContext Current => _current.Value; + + /// + /// Begins a new invocation scope with a fresh . + /// The returned must be disposed to clear the context. + /// + /// A disposable scope that clears the context on disposal. + public static Scope Begin() + { + return new(new AIInvocationContext()); + } + + /// + /// Begins a new invocation scope with the specified . + /// The returned must be disposed to clear the context. + /// + /// The invocation context to make current. + /// A disposable scope that clears the context on disposal. + public static Scope Begin(AIInvocationContext context) + { + ArgumentNullException.ThrowIfNull(context); + return new Scope(context); + } + + /// + /// A disposable wrapper that sets and clears the context. + /// + public readonly struct Scope : IDisposable + { + /// + /// Gets the associated with this scope. + /// + public AIInvocationContext Context { get; } + + internal Scope(AIInvocationContext context) + { + Context = context; + _current.Value = context; + } + + /// + /// Clears the current invocation context, preventing data from leaking + /// into subsequent operations on the same thread. + /// + public void Dispose() + { + _current.Value = null; + } + } +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/DocumentIndexContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/DocumentIndexContext.cs new file mode 100644 index 00000000..abf86c5f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/DocumentIndexContext.cs @@ -0,0 +1,29 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; + +namespace CrestApps.Core.AI.Orchestration; + +/// +/// Context for document indexing operations. +/// +public sealed class DocumentIndexContext +{ + /// + /// Gets or sets the session/interaction identifier. + /// + public string SessionId { get; set; } + + /// + /// Gets or sets the document identifier. + /// + public string DocumentId { get; set; } + + /// + /// Gets or sets the original file name. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the document chunks with embeddings. + /// + public IList Chunks { get; set; } = []; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrationContextBuilder.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrationContextBuilder.cs new file mode 100644 index 00000000..600808b3 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrationContextBuilder.cs @@ -0,0 +1,31 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Orchestration; + +/// +/// Builds instances from a given resource object. +/// +/// +/// The default implementation creates an empty context, then executes the +/// registered pipeline in the following order: +/// 1) , +/// 2) the optional delegate, +/// 3) . +/// +public interface IOrchestrationContextBuilder +{ + /// + /// Creates and configures a new based on the provided . + /// + /// + /// The resource object (e.g., or ChatInteraction) used to + /// seed and configure the orchestration context. Must not be . + /// + /// + /// An optional delegate to override or fine-tune the context after handlers have run + /// BuildingAsync but before BuiltAsync. + /// + /// A task that completes with the fully built . + /// Thrown if is . + ValueTask BuildAsync(object resource, Action configure = null); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrationContextBuilderHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrationContextBuilderHandler.cs new file mode 100644 index 00000000..ce03e508 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrationContextBuilderHandler.cs @@ -0,0 +1,32 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Orchestration; + +/// +/// Handles lifecycle events raised while building an . +/// +/// +/// Implementations can enrich, validate, or otherwise mutate the context. The builder will invoke +/// first, then apply any caller-provided +/// configuration, and finally invoke . +/// Handlers are resolved from DI and executed in reverse registration order to allow last-registered +/// handlers to run first. +/// +public interface IOrchestrationContextBuilderHandler +{ + /// + /// Called while the is being constructed, before the optional caller + /// configuration delegate is applied. + /// + /// Carries both the source resource and the mutable . + /// A task that completes when the mutation or validation is done. + Task BuildingAsync(OrchestrationContextBuildingContext context); + + /// + /// Called after the context has been fully constructed and the optional caller configuration delegate + /// has been applied. + /// + /// Carries the final along with the source resource. + /// A task that completes when post-build processing is done. + Task BuiltAsync(OrchestrationContextBuiltContext context); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrator.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrator.cs new file mode 100644 index 00000000..f2ab09b7 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestrator.cs @@ -0,0 +1,39 @@ +using CrestApps.Core.AI.Models; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Orchestration; + +/// +/// Defines a pluggable orchestration runtime responsible for planning, tool scoping, +/// and managing the execution loop for an AI completion session. +/// +/// +/// Each chat session binds to exactly one orchestrator runtime for its lifetime. +/// The orchestrator is resolved per session based on the AI profile or chat interaction configuration. +/// Implementations should handle: +/// +/// Planning the task (decomposing complex requests into steps) +/// Scoping relevant tools (selecting a subset of available tools) +/// Executing iterative agent loops (calling the LLM with scoped tools) +/// Detecting capability gaps and expanding the tool scope progressively +/// Producing the final streaming response +/// +/// +public interface IOrchestrator +{ + /// + /// Gets the unique name of this orchestrator. + /// + string Name { get; } + + /// + /// Executes the orchestration pipeline and yields streaming completion updates. + /// + /// The orchestration context containing the user message, + /// conversation history, completion settings, document references, and extensible properties. + /// A token to cancel the operation. + /// An asynchronous stream of completion updates. + IAsyncEnumerable ExecuteStreamingAsync( + OrchestrationContext context, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestratorAvailabilityProvider.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestratorAvailabilityProvider.cs new file mode 100644 index 00000000..0a2dc90f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestratorAvailabilityProvider.cs @@ -0,0 +1,20 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Orchestration; + +/// +/// Provides availability state for a specific orchestrator so callers can block +/// requests before attempting to execute an unavailable orchestrator. +/// +public interface IOrchestratorAvailabilityProvider +{ + /// + /// Gets the orchestrator name this provider applies to. + /// + string OrchestratorName { get; } + + /// + /// Gets the current availability state for the orchestrator. + /// + Task GetAvailabilityAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestratorResolver.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestratorResolver.cs new file mode 100644 index 00000000..ed92d100 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IOrchestratorResolver.cs @@ -0,0 +1,20 @@ +namespace CrestApps.Core.AI.Orchestration; + +/// +/// Resolves the appropriate for a chat session based on +/// the configured orchestrator name. +/// +/// +/// Resolution order: explicit name → system default. +/// If no name is specified or the named orchestrator is not found, the system default is returned. +/// +public interface IOrchestratorResolver +{ + /// + /// Resolves an orchestrator by name. Returns the system default if the name is + /// , empty, or unrecognized. + /// + /// The orchestrator name, or for the default. + /// The resolved instance. + IOrchestrator Resolve(string orchestratorName = null); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IPreemptiveRagHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IPreemptiveRagHandler.cs new file mode 100644 index 00000000..b1d3138e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Orchestration/IPreemptiveRagHandler.cs @@ -0,0 +1,25 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Memory; + +/// +/// Defines a handler that processes preemptive RAG (Retrieval-Augmented Generation) for a specific +/// data source type. Implementations receive pre-extracted search queries and are responsible for +/// embedding them, performing vector search, and injecting relevant context into the system message. +/// +public interface IPreemptiveRagHandler +{ + /// + /// Determines asynchronously whether the specified context can be handled by this instance. + /// + /// The context to evaluate for handling. Cannot be null. + /// A task that represents the asynchronous operation. The task result is if the context can + /// be handled; otherwise, . + ValueTask CanHandleAsync(OrchestrationContextBuiltContext context); + + /// + /// Handles preemptive RAG injection for the given context and search queries. + /// + /// The preemptive RAG context containing the orchestration state and extracted queries. + Task HandleAsync(PreemptiveRagContext context); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileManager.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileManager.cs new file mode 100644 index 00000000..ea149e9a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileManager.cs @@ -0,0 +1,18 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Profiles; +/// +/// Manages AI profiles with CRUD operations, name-based lookup, +/// and type-filtered retrieval. AI profiles define chat, agent, and embedding +/// configurations that drive AI completion behavior. +/// +public interface IAIProfileManager : INamedCatalogManager +{ + /// + /// Asynchronously retrieves a collection of AI chat profiles of the specified type. + /// + /// The type of AI chat profiles to retrieve. + /// A ValueTask that represents the asynchronous operation. The result is an enumerable collection of AIProfile objects matching the specified type. + ValueTask> GetAsync(AIProfileType type); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileStore.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileStore.cs new file mode 100644 index 00000000..0c407ff3 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileStore.cs @@ -0,0 +1,20 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Services; +namespace CrestApps.Core.AI.Profiles; +/// +/// Provides persistent storage for AI profiles, supporting CRUD operations, +/// name-based lookup, and efficient type-filtered queries via index. +/// +public interface IAIProfileStore : INamedCatalog +{ + /// + /// Asynchronously retrieves a collection of AI profiles of the specified type + /// using an efficient index query rather than loading all profiles. + /// + /// The type of AI profiles to retrieve. + /// + /// A representing the asynchronous operation. + /// The result is a read-only collection of matching the specified type. + /// + ValueTask> GetByTypeAsync(AIProfileType type); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileTemplateManager.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileTemplateManager.cs new file mode 100644 index 00000000..2fa904f5 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileTemplateManager.cs @@ -0,0 +1,17 @@ +using CrestApps.Core.AI.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.AI.Profiles; + +/// +/// Manages AI profile templates with unified access to both +/// database-stored and file-based template sources. +/// +public interface IAIProfileTemplateManager : INamedSourceCatalogManager +{ + /// + /// Gets all listable profile templates from all sources + /// (database and file-based providers). + /// + ValueTask> GetListableAsync(); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileTemplateProvider.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileTemplateProvider.cs new file mode 100644 index 00000000..5351abb5 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIProfileTemplateProvider.cs @@ -0,0 +1,14 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Profiles; + +/// +/// Provides AI profile templates from a specific source (e.g., module files). +/// +public interface IAIProfileTemplateProvider +{ + /// + /// Gets all profile templates from this provider. + /// + Task> GetTemplatesAsync(); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIReferenceLinkResolver.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIReferenceLinkResolver.cs new file mode 100644 index 00000000..fd1f4154 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Profiles/IAIReferenceLinkResolver.cs @@ -0,0 +1,17 @@ +namespace CrestApps.Core.AI.Profiles; + +/// +/// Resolves links for AI completion references based on the reference type. +/// Implementations are registered as keyed services using the reference type as the key, +/// allowing different strategies for different source types (e.g., content items, documents, custom). +/// +public interface IAIReferenceLinkResolver +{ + /// + /// Resolves a link URL for the given reference. + /// + /// The unique identifier of the referenced resource. + /// Optional metadata associated with the reference. + /// The resolved link URL, or null if no link can be generated. + string ResolveLink(string referenceId, IDictionary metadata); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Properties/AssemblyInfo.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..c86aed33 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("CrestApps.Core.AI")] +[assembly: InternalsVisibleTo("CrestApps.OrchardCore.Tests")] +[assembly: InternalsVisibleTo("CrestApps.Core.Tests")] diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerContext.cs new file mode 100644 index 00000000..419dde79 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerContext.cs @@ -0,0 +1,75 @@ +using CrestApps.Core.AI.Models; +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.ResponseHandling; + +/// +/// Provides context to an when processing a chat prompt. +/// Contains the user's prompt, connection details, session state, conversation history, +/// and references to the underlying session or interaction object. +/// +public sealed class ChatResponseHandlerContext +{ + /// + /// Gets the user's prompt text. + /// + public required string Prompt { get; init; } + + /// + /// Gets the SignalR connection ID of the client. + /// This can be used by deferred handlers to send responses back to the client + /// at a later time, or to target a SignalR group for reconnection support. + /// + public required string ConnectionId { get; init; } + + /// + /// Gets the session identifier. + /// For , this is . + /// For , this is the ChatInteraction.ItemId. + /// + public required string SessionId { get; init; } + + /// + /// Gets the type of chat context. + /// + public required ChatContextType ChatType { get; init; } + + /// + /// Gets the conversation history (prior messages in the session). + /// + public required IList ConversationHistory { get; init; } + + /// + /// Gets the scoped service provider for resolving services. + /// + public required IServiceProvider Services { get; init; } + + /// + /// Gets the AI profile associated with this session. + /// Only set when is . + /// + public AIProfile Profile { get; init; } + + /// + /// Gets the chat session. + /// Only set when is . + /// + public AIChatSession ChatSession { get; init; } + + /// + /// Gets the chat interaction. + /// Only set when is . + /// + public ChatInteraction Interaction { get; init; } + + /// + /// Gets or sets the assistant message appearance that should be applied to + /// assistant messages streamed by the current handler invocation. + /// + public AssistantMessageAppearance AssistantAppearance { get; set; } + + /// + /// Gets or sets an extensible property bag for passing additional data to handlers. + /// + public Dictionary Properties { get; init; } = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerNames.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerNames.cs new file mode 100644 index 00000000..0cbe0331 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerNames.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core.AI.ResponseHandling; + +/// +/// Defines well-known names. +/// +public static class ChatResponseHandlerNames +{ + /// + /// The well-known name for the built-in AI handler that routes prompts + /// through the AI orchestration pipeline. + /// + public const string AI = "AI"; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerResult.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerResult.cs new file mode 100644 index 00000000..2a1a6737 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ChatResponseHandlerResult.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.ResponseHandling; +/// +/// Represents the result of an processing a chat prompt. +/// A result is either streaming (the response is available immediately as a stream +/// of chunks) or deferred (the response will be +/// delivered asynchronously at a later time, e.g., via webhook). +/// +public sealed class ChatResponseHandlerResult +{ + /// + /// Gets a value indicating whether the response is deferred. + /// When , the hub should save the user prompt and complete + /// without waiting for an assistant response. The response will be delivered + /// asynchronously (e.g., via webhook, background task, or external callback). + /// + public bool IsDeferred { get; init; } + /// + /// Gets the streaming response. Only available when is . + /// Each contains a partial text chunk to be streamed to the client. + /// + public IAsyncEnumerable ResponseStream { get; init; } + + /// + /// Creates a deferred result, indicating the response will arrive later. + /// + /// A new with set to . + public static ChatResponseHandlerResult Deferred() + { + return new() + { + IsDeferred = true + }; + } + + /// + /// Creates a streaming result with the given response stream. + /// + /// The asynchronous stream of response updates. + /// A new with the given . + public static ChatResponseHandlerResult Streaming(IAsyncEnumerable stream) + { + return new() + { + ResponseStream = stream + }; + } +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/IChatResponseHandler.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/IChatResponseHandler.cs new file mode 100644 index 00000000..e0e0b542 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/IChatResponseHandler.cs @@ -0,0 +1,42 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.ResponseHandling; + +/// +/// Defines a pluggable handler responsible for processing chat prompts and producing responses. +/// Implementations may generate responses in real time (streaming) or defer them +/// for asynchronous delivery (e.g., via webhook from a third-party agent platform). +/// +/// +/// The default implementation routes prompts through the AI orchestration pipeline. +/// Custom implementations can route prompts to external systems such as live-agent +/// platforms (e.g., Genesys, Twilio Flex) or any other backend capable of handling +/// human-to-human or hybrid conversations. +/// +/// The active handler for a session is determined by +/// or +/// . An AI function or external +/// event can change this value mid-conversation to transfer the chat to a different handler. +/// +public interface IChatResponseHandler +{ + /// + /// Gets the unique technical name of this handler (e.g., "AI", "Genesys"). + /// + string Name { get; } + /// + /// Processes a chat prompt and returns a result indicating whether the response + /// is available immediately (streaming) or will be delivered later (deferred). + /// + /// The context describing the prompt, session, and connection details. + /// A token to cancel the operation. + /// + /// A that is either streaming (contains an + /// of updates) or deferred (the hub will not wait + /// for a response). + /// + + Task HandleAsync( + ChatResponseHandlerContext context, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/IChatResponseHandlerResolver.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/IChatResponseHandlerResolver.cs new file mode 100644 index 00000000..51983664 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/IChatResponseHandlerResolver.cs @@ -0,0 +1,39 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.ResponseHandling; + +/// +/// Resolves the appropriate for a chat session based +/// on the configured handler name. +/// +/// +/// Resolution order: explicit name → default AI handler. +/// When is or empty, the built-in AI +/// handler is returned. When a name is specified but no matching handler is found, +/// implementations should fall back to the default AI handler. +/// When is , +/// the AI handler is always returned regardless of the requested name because +/// conversation mode requires the AI orchestration pipeline for speech-to-text +/// and text-to-speech integration. +/// +public interface IChatResponseHandlerResolver +{ + /// + /// Resolves a chat response handler by name. + /// + /// + /// The handler name, or / empty for the default AI handler. + /// + /// + /// The active chat mode. When set to , the + /// built-in AI handler is always returned regardless of . + /// + /// The resolved instance. + IChatResponseHandler Resolve(string handlerName = null, ChatMode chatMode = ChatMode.TextInput); + + /// + /// Gets all registered instances. + /// + /// An enumerable of all registered handlers. + IEnumerable GetAll(); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ResponseHandlerProfileSettings.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ResponseHandlerProfileSettings.cs new file mode 100644 index 00000000..d511bb41 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/ResponseHandling/ResponseHandlerProfileSettings.cs @@ -0,0 +1,25 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.ResponseHandling; + +/// +/// Settings stored on to control the initial +/// response handler for new chat sessions using this profile. +/// +/// +/// When is or empty, +/// the default AI handler processes all prompts. Set this to a specific handler name +/// (e.g., "Genesys") to route prompts to that handler from the start of the session. +/// The active handler can be changed mid-conversation by an AI function or external event +/// by updating or +/// . +/// +public sealed class ResponseHandlerProfileSettings +{ + /// + /// Gets or sets the name of the initial + /// for new sessions created from this profile. + /// When or empty, the default AI handler is used. + /// + public string InitialResponseHandlerName { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Services/IAITextNormalizer.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Services/IAITextNormalizer.cs new file mode 100644 index 00000000..e60f311f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Services/IAITextNormalizer.cs @@ -0,0 +1,14 @@ +namespace CrestApps.Core.AI.Services; + +/// +/// Normalizes titles and content used by RAG pipelines, and chunks normalized +/// content for embedding/indexing when needed. +/// +public interface IAITextNormalizer +{ + Task NormalizeContentAsync(string text, CancellationToken cancellationToken = default); + + Task> NormalizeAndChunkAsync(string text, CancellationToken cancellationToken = default); + + string NormalizeTitle(string title); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Speech/ISpeechVoiceResolver.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Speech/ISpeechVoiceResolver.cs new file mode 100644 index 00000000..9e0702a8 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Speech/ISpeechVoiceResolver.cs @@ -0,0 +1,16 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Speech; + +/// +/// Resolves the available speech voices for a deployment by delegating to the matching AI client provider. +/// +public interface ISpeechVoiceResolver +{ + /// + /// Gets the available speech voices for the specified deployment. + /// + /// The AI deployment containing provider, connection, and model information. + /// An array of available instances. + Task GetSpeechVoicesAsync(AIDeployment deployment); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Speech/ITextTokenizer.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Speech/ITextTokenizer.cs new file mode 100644 index 00000000..0565cc45 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Speech/ITextTokenizer.cs @@ -0,0 +1,18 @@ +namespace CrestApps.Core.AI.Speech; + +/// +/// Tokenizes text into a set of normalized terms for matching and scoring. +/// +/// +/// Implementations should handle code identifiers (camelCase/PascalCase splitting), +/// stop word removal, stemming, and case normalization for optimal matching results. +/// +public interface ITextTokenizer +{ + /// + /// Tokenizes the given text into a set of distinct, normalized tokens. + /// + /// The text to tokenize. + /// A set of unique tokens, or an empty set if the input is null or whitespace. + HashSet Tokenize(string text); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolBuilder.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolBuilder.cs new file mode 100644 index 00000000..0c96a978 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolBuilder.cs @@ -0,0 +1,67 @@ +using Microsoft.Extensions.AI; + +namespace CrestApps.Core.AI.Tooling; + +/// +/// A fluent builder for configuring an AI tool registration. +/// By default, tools are registered as system tools (not visible in the UI). +/// Call to make the tool visible in the UI for user selection. +/// +/// The tool type implementing . +public sealed class AIToolBuilder + where TTool : AITool +{ + private readonly AIToolDefinitionEntry _entry; + + internal AIToolBuilder(AIToolDefinitionEntry entry) + { + _entry = entry; + } + /// + /// Sets the display title for this tool. + /// + public AIToolBuilder WithTitle(string title) + { + _entry.Title = title; + + return this; + } + /// + /// Sets the description for this tool. + /// + public AIToolBuilder WithDescription(string description) + { + _entry.Description = description; + + return this; + } + /// + /// Sets the category for grouping this tool in the UI. + /// + public AIToolBuilder WithCategory(string category) + { + _entry.Category = category; + + return this; + } + /// + /// Sets the purpose tag for this tool. Use well-known constants from + /// or define custom purpose strings for domain-specific tool grouping. + /// + public AIToolBuilder WithPurpose(string purpose) + { + _entry.Purpose = purpose; + + return this; + } + /// + /// Makes this tool visible in the UI for user selection. + /// By default, tools are system tools managed by the orchestrator and are not shown in the UI. + /// + public AIToolBuilder Selectable() + { + _entry.IsSystemTool = false; + + return this; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolDefinitionEntry.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolDefinitionEntry.cs new file mode 100644 index 00000000..fcc771a8 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolDefinitionEntry.cs @@ -0,0 +1,48 @@ +namespace CrestApps.Core.AI.Tooling; + +public sealed class AIToolDefinitionEntry +{ + public AIToolDefinitionEntry(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + ToolType = type; + } + + public Type ToolType { get; } + + public string Title { get; set; } + + public string Description { get; set; } + + public string Category { get; set; } + + public string Name { get; internal set; } + + /// + /// Gets or sets whether this tool is a system tool. System tools are automatically + /// included by the orchestrator based on context availability and are not shown + /// in the UI tool selection. + /// + public bool IsSystemTool { get; set; } + + /// + /// Gets or sets the purpose tag for this tool. Use well-known constants from + /// or custom strings for domain-specific grouping. + /// The orchestrator uses this to dynamically discover tools by purpose + /// (e.g., document processing tools for enriching system messages). + /// + public string Purpose { get; set; } + + public bool HasPurpose(string purpose) + { + ArgumentException.ThrowIfNullOrEmpty(purpose); + + if (Purpose == null) + { + return false; + } + + return string.Equals(Purpose, purpose, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolDefinitionOptions.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolDefinitionOptions.cs new file mode 100644 index 00000000..93f320bd --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolDefinitionOptions.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core.AI.Tooling; + +public sealed class AIToolDefinitionOptions +{ + private readonly Dictionary _tools = new(StringComparer.OrdinalIgnoreCase); + + public IReadOnlyDictionary Tools => _tools; + + internal void SetTool(string name, AIToolDefinitionEntry entry) + { + _tools[name] = entry; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolExecutionContext.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolExecutionContext.cs new file mode 100644 index 00000000..15975581 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolExecutionContext.cs @@ -0,0 +1,37 @@ +using CrestApps.Core.AI.Orchestration; + +namespace CrestApps.Core.AI.Tooling; + +/// +/// Ambient context that provides AI tool implementations with +/// the provider, connection, and resource information of the current request. +/// Stored in and accessible +/// via AIInvocationScope.Current?.ToolExecutionContext. +/// +public sealed class AIToolExecutionContext +{ + /// + /// Gets or sets the name of the AI provider handling the current request + /// (e.g., "OpenAI", "AzureOpenAI", "Ollama"). + /// + public string ProviderName { get; set; } + + /// + /// Gets or sets the connection name within the provider. + /// + public string ConnectionName { get; set; } + + /// + /// Gets the resource (e.g., or + /// ) that initiated the current AI request. + /// Tools can cast this to access resource-specific data such as interaction IDs. + /// + public object Resource { get; } + + public AIToolExecutionContext(object resource) + { + ArgumentNullException.ThrowIfNull(resource); + + Resource = resource; + } +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolPurposes.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolPurposes.cs new file mode 100644 index 00000000..c085297d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/AIToolPurposes.cs @@ -0,0 +1,29 @@ +namespace CrestApps.Core.AI.Tooling; + +/// +/// Defines well-known purpose identifiers for AI tools. +/// Use these constants with +/// to tag tools with their intended purpose. +/// +public static class AIToolPurposes +{ + /// + /// Tools that process, read, search, or manage documents attached to a chat session. + /// + public const string DocumentProcessing = "document_processing"; + + /// + /// Tools that generate content such as images or charts. + /// + public const string ContentGeneration = "content_generation"; + + /// + /// Tools that search data source embeddings for RAG (Retrieval-Augmented Generation). + /// + public const string DataSourceSearch = "data_source_search"; + + /// + /// Tools that recall, list, or store authenticated user memory. + /// + public const string Memory = "memory"; +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IAIToolAccessEvaluator.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IAIToolAccessEvaluator.cs new file mode 100644 index 00000000..81f98f89 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IAIToolAccessEvaluator.cs @@ -0,0 +1,17 @@ +using System.Security.Claims; + +namespace CrestApps.Core.AI.Tooling; + +/// +/// Evaluates whether a given user is authorized to use a specific AI tool. +/// +public interface IAIToolAccessEvaluator +{ + /// + /// Determines whether the specified user is allowed to invoke the given tool. + /// + /// The current user principal. May be null for anonymous requests. + /// The name of the AI tool to authorize. + /// true if the user is authorized; otherwise false. + Task IsAuthorizedAsync(ClaimsPrincipal user, string toolName); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IToolRegistry.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IToolRegistry.cs new file mode 100644 index 00000000..21cdf8c2 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IToolRegistry.cs @@ -0,0 +1,32 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Tooling; + +/// +/// A unified index of all available tools (local and MCP) that supports retrieval +/// and relevance-based searching for tool scoping during orchestration. +/// +public interface IToolRegistry +{ + /// + /// Gets all tool entries scoped to the given completion context. + /// + Task> GetAllAsync( + AICompletionContext context, + CancellationToken cancellationToken = default); + + /// + /// Searches for the most relevant tools based on a capability query string. + /// Returns the top-K entries ranked by relevance. + /// + /// A capability description or user intent to match against tool metadata. + /// The maximum number of results to return. + /// The completion context that scopes available tools. + /// A token to cancel the operation. + /// A ranked list of the most relevant tool entries. + Task> SearchAsync( + string query, + int topK, + AICompletionContext context, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IToolRegistryProvider.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IToolRegistryProvider.cs new file mode 100644 index 00000000..2be073cb --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/IToolRegistryProvider.cs @@ -0,0 +1,21 @@ +using CrestApps.Core.AI.Models; + +namespace CrestApps.Core.AI.Tooling; + +/// +/// Provides tool metadata entries to the unified tool registry. +/// Implementations supply tools from different sources (local registrations, MCP servers, etc.). +/// +public interface IToolRegistryProvider +{ + /// + /// Retrieves all tool entries available from this provider, scoped to the given context. + /// + /// The completion context containing configured tool names, + /// instance IDs, and MCP connection IDs that scope the returned tools. + /// A token to cancel the operation. + /// A read-only list of tool registry entries from this provider. + Task> GetToolsAsync( + AICompletionContext context, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/SystemToolNames.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/SystemToolNames.cs new file mode 100644 index 00000000..07e8d1a7 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Tooling/SystemToolNames.cs @@ -0,0 +1,16 @@ +namespace CrestApps.Core.AI.Tooling; + +/// +/// Provides well-known names for system tools that are automatically +/// included by the orchestrator based on context availability. +/// +public static class SystemToolNames +{ + public const string ListDocuments = "list_documents"; + public const string ReadDocument = "read_document"; + public const string SearchDocuments = "search_documents"; + public const string SearchDataSources = "search_data_sources"; + public const string ReadTabularData = "read_tabular_data"; + public const string GenerateImage = "generate_image"; + public const string GenerateChart = "generate_chart"; +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Builders/CrestAppsBuilder.cs b/src/Abstractions/CrestApps.Core.Abstractions/Builders/CrestAppsBuilder.cs new file mode 100644 index 00000000..f4c1a785 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Builders/CrestAppsBuilder.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace CrestApps.Core.Builders; + +public class CrestAppsCoreBuilder +{ + public CrestAppsCoreBuilder(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + Services = services; + } + + public IServiceCollection Services { get; } +} + +public sealed class CrestAppsAISuiteBuilder +{ + public CrestAppsAISuiteBuilder(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + Services = services; + } + + public IServiceCollection Services { get; } +} + +public sealed class CrestAppsIndexingBuilder +{ + public CrestAppsIndexingBuilder(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + Services = services; + } + + public IServiceCollection Services { get; } +} + +public sealed class CrestAppsChatInteractionsBuilder +{ + public CrestAppsChatInteractionsBuilder(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + Services = services; + } + + public IServiceCollection Services { get; } +} + +public sealed class CrestAppsDocumentProcessingBuilder +{ + public CrestAppsDocumentProcessingBuilder(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + Services = services; + } + + public IServiceCollection Services { get; } +} + +public sealed class CrestAppsMcpServerBuilder +{ + public CrestAppsMcpServerBuilder(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + Services = services; + } + + public IServiceCollection Services { get; } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/CrestApps.Core.Abstractions.csproj b/src/Abstractions/CrestApps.Core.Abstractions/CrestApps.Core.Abstractions.csproj new file mode 100644 index 00000000..637d3c2f --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/CrestApps.Core.Abstractions.csproj @@ -0,0 +1,16 @@ + + + CrestApps.Core + CrestApps Abstractions + + $(CrestAppsDescription) + + Core abstractions for CrestApps services. Framework-independent, usable in any ASP.NET Core application. + + $(PackageTags) Abstractions + + + + + + diff --git a/src/Abstractions/CrestApps.Core.Abstractions/CrestAppsConstants.cs b/src/Abstractions/CrestApps.Core.Abstractions/CrestAppsConstants.cs new file mode 100644 index 00000000..41d831a1 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/CrestAppsConstants.cs @@ -0,0 +1,10 @@ +namespace CrestApps.Core; + +public static class CrestAppsConstants +{ + public const string Author = "The CrestApps Team"; + + public const string Version = "2.0.0"; + + public const string Website = "https://crestapps.com"; +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/ExtensibleEntity.cs b/src/Abstractions/CrestApps.Core.Abstractions/ExtensibleEntity.cs new file mode 100644 index 00000000..40121a32 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/ExtensibleEntity.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CrestApps.Core; + +/// +/// Base class for entities that support dynamic extensible properties. +/// +public abstract class ExtensibleEntity +{ + [JsonExtensionData] + public IDictionary Properties { get; set; } = new Dictionary(); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/ExtensibleEntityExtensions.cs b/src/Abstractions/CrestApps.Core.Abstractions/ExtensibleEntityExtensions.cs new file mode 100644 index 00000000..cf4ae38a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/ExtensibleEntityExtensions.cs @@ -0,0 +1,161 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace CrestApps.Core; + +/// +/// Extension methods for to provide dynamic property storage, +/// matching the patterns from OrchardCore.Entities.Entity. +/// +public static class ExtensibleEntityExtensions +{ + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + /// + /// Gets a strongly-typed object stored in the entity's properties. + /// + public static T As(this ExtensibleEntity entity) + where T : new() + { + ArgumentNullException.ThrowIfNull(entity); + + var key = typeof(T).Name; + + return entity.Properties.TryGetValue(key, out var value) + ? DeserializeValue(value) ?? new T() + : new T(); + } + + /// + /// Gets a strongly-typed object stored in the entity's properties, or null if not found. + /// + public static T Get(this ExtensibleEntity entity, string name) + { + ArgumentNullException.ThrowIfNull(entity); + ArgumentException.ThrowIfNullOrEmpty(name); + + return entity.Properties.TryGetValue(name, out var value) + ? DeserializeValue(value) + : default; + } + + /// + /// Stores a strongly-typed object in the entity's properties using the type name as key. + /// + public static ExtensibleEntity Put(this ExtensibleEntity entity, T value) + where T : new() + { + ArgumentNullException.ThrowIfNull(entity); + + entity.Properties[typeof(T).Name] = value; + + return entity; + } + + /// + /// Stores a value in the entity's properties using a named key. + /// + public static ExtensibleEntity Put(this ExtensibleEntity entity, string name, object value) + { + ArgumentNullException.ThrowIfNull(entity); + ArgumentException.ThrowIfNullOrEmpty(name); + + entity.Properties[name] = value; + + return entity; + } + + /// + /// Tries to get a strongly-typed object stored in the entity's properties. + /// Returns true if a non-null value was found and deserialized. + /// + public static bool TryGet(this ExtensibleEntity entity, out T result) + where T : class, new() + { + ArgumentNullException.ThrowIfNull(entity); + + var key = typeof(T).Name; + + if (entity.Properties.TryGetValue(key, out var value) && value is not null) + { + result = DeserializeValue(value); + return result is not null; + } + + result = default; + return false; + } + + /// + /// Checks if the entity's properties contain a key with the given type name. + /// + public static bool Has(this ExtensibleEntity entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return entity.Properties.ContainsKey(typeof(T).Name); + } + + /// + /// Modifies a stored object in-place. If no object exists, a new instance is created, + /// modified, and stored. + /// + public static ExtensibleEntity Alter(this ExtensibleEntity entity, Action alter) + where T : new() + { + ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(alter); + + var obj = entity.As(); + alter(obj); + entity.Put(obj); + + return entity; + } + + /// + /// Removes a stored object from the entity's properties using the type name as key. + /// + public static ExtensibleEntity Remove(this ExtensibleEntity entity) + { + ArgumentNullException.ThrowIfNull(entity); + + entity.Properties.Remove(typeof(T).Name); + + return entity; + } + + private static T DeserializeValue(object value) + { + if (value is null) + { + return default; + } + + if (value is T typed) + { + return typed; + } + + if (value is JsonElement jsonElement) + { + if (jsonElement.ValueKind == JsonValueKind.Null) + { + return default; + } + + return jsonElement.Deserialize(_jsonOptions); + } + + if (value is JsonNode jsonNode) + { + return jsonNode.Deserialize(_jsonOptions); + } + + var json = JsonSerializer.Serialize(value, _jsonOptions); + return JsonSerializer.Deserialize(json, _jsonOptions); + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/IDisplayTextAwareModel.cs b/src/Abstractions/CrestApps.Core.Abstractions/IDisplayTextAwareModel.cs new file mode 100644 index 00000000..288f70ef --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/IDisplayTextAwareModel.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core; + +/// +/// Marks a model as having a human-readable display text property, +/// enabling consistent rendering in lists, dropdowns, and search results. +/// +public interface IDisplayTextAwareModel +{ + /// + /// Gets or sets the human-readable display text for this model. + /// + string DisplayText { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/INameAwareModel.cs b/src/Abstractions/CrestApps.Core.Abstractions/INameAwareModel.cs new file mode 100644 index 00000000..f78b296d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/INameAwareModel.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core; + +/// +/// Marks a model as having a unique technical name property, +/// enabling lookup and identification by a stable, human-friendly key. +/// +public interface INameAwareModel +{ + /// + /// Gets or sets the unique technical name for this model. + /// + string Name { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/ISourceAwareModel.cs b/src/Abstractions/CrestApps.Core.Abstractions/ISourceAwareModel.cs new file mode 100644 index 00000000..e614c623 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/ISourceAwareModel.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core; + +/// +/// Marks a model as being associated with a named origin or provider, +/// enabling filtering and grouping by source (e.g., "OpenAI", "AzureOpenAI"). +/// +public interface ISourceAwareModel +{ + /// + /// Gets or sets the name of the source or provider that owns this model. + /// + string Source { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/JsonExtensions.cs b/src/Abstractions/CrestApps.Core.Abstractions/JsonExtensions.cs new file mode 100644 index 00000000..5c1707fc --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/JsonExtensions.cs @@ -0,0 +1,58 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace CrestApps.Core; + +/// +/// Extension methods for JSON types to replace OrchardCore's JSON helpers. +/// +public static class JsonExtensions +{ + private static readonly JsonSerializerOptions _defaultOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + /// + /// Creates a from an object. + /// + public static JsonObject FromObject(T value, JsonSerializerOptions options = null) + { + if (value is null) + { + return []; + } + + var json = JsonSerializer.Serialize(value, options ?? _defaultOptions); + + return JsonNode.Parse(json)?.AsObject() ?? []; + } + + /// + /// Deep-clones an used for extensible properties. + /// + public static IDictionary Clone(this IDictionary properties) + { + if (properties is null || properties.Count == 0) + { + return new Dictionary(); + } + + var json = JsonSerializer.Serialize(properties); + + return JsonSerializer.Deserialize>(json) ?? new Dictionary(); + } + + /// + /// Deep-clones a . + /// + public static JsonObject Clone(this JsonObject jsonObject) + { + if (jsonObject is null) + { + return []; + } + + return jsonObject.DeepClone().AsObject(); + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/CatalogItem.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/CatalogItem.cs new file mode 100644 index 00000000..40211cee --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/CatalogItem.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public class CatalogItem : ExtensibleEntity +{ + /// + /// Gets or sets the unique identifier for the catalog item. + /// + public string ItemId { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/CreatedContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/CreatedContext.cs new file mode 100644 index 00000000..f92930a8 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/CreatedContext.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public sealed class CreatedContext : HandlerContextBase +{ + public CreatedContext(T model) + : base(model) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/CreatingContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/CreatingContext.cs new file mode 100644 index 00000000..f00262ae --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/CreatingContext.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public sealed class CreatingContext : HandlerContextBase +{ + public CreatingContext(T model) + : base(model) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/DeletedContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/DeletedContext.cs new file mode 100644 index 00000000..ea090433 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/DeletedContext.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public sealed class DeletedContext : HandlerContextBase +{ + public DeletedContext(T entry) + : base(entry) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/DeletingContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/DeletingContext.cs new file mode 100644 index 00000000..db9897f1 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/DeletingContext.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public sealed class DeletingContext : HandlerContextBase +{ + public DeletingContext(T model) + : base(model) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/HandlerContextBase.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/HandlerContextBase.cs new file mode 100644 index 00000000..f487e3d4 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/HandlerContextBase.cs @@ -0,0 +1,13 @@ +namespace CrestApps.Core.Models; + +public abstract class HandlerContextBase +{ + public T Model { get; } + + public HandlerContextBase(T model) + { + ArgumentNullException.ThrowIfNull(model); + + Model = model; + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/InitializedContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/InitializedContext.cs new file mode 100644 index 00000000..fe00247c --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/InitializedContext.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public sealed class InitializedContext : HandlerContextBase +{ + public InitializedContext(T model) + : base(model) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/InitializingContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/InitializingContext.cs new file mode 100644 index 00000000..cca8422a --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/InitializingContext.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Nodes; + +namespace CrestApps.Core.Models; + +public sealed class InitializingContext : HandlerContextBase +{ + public JsonNode Data { get; } + + public InitializingContext(T model, JsonNode data) + : base(model) + { + Data = data ?? new JsonObject(); + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/LoadedContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/LoadedContext.cs new file mode 100644 index 00000000..8ab2a2ea --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/LoadedContext.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public sealed class LoadedContext : HandlerContextBase +{ + public LoadedContext(T model) + : base(model) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/PageResult.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/PageResult.cs new file mode 100644 index 00000000..eccaf9b5 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/PageResult.cs @@ -0,0 +1,8 @@ +namespace CrestApps.Core.Models; + +public class PageResult +{ + public int Count { get; set; } + + public IReadOnlyCollection Entries { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/QueryContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/QueryContext.cs new file mode 100644 index 00000000..be0c6c96 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/QueryContext.cs @@ -0,0 +1,10 @@ +namespace CrestApps.Core.Models; + +public class QueryContext +{ + public string Source { get; set; } + + public string Name { get; set; } + + public bool Sorted { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/SourceCatalogEntry.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/SourceCatalogEntry.cs new file mode 100644 index 00000000..f6e53e29 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/SourceCatalogEntry.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public class SourceCatalogEntry : CatalogItem, ISourceAwareModel +{ + /// + /// Gets the name of the source for this profile. + /// + public string Source { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/UpdatedContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/UpdatedContext.cs new file mode 100644 index 00000000..fc78bbf4 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/UpdatedContext.cs @@ -0,0 +1,9 @@ +namespace CrestApps.Core.Models; + +public sealed class UpdatedContext : HandlerContextBase +{ + public UpdatedContext(T model) + : base(model) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/UpdatingContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/UpdatingContext.cs new file mode 100644 index 00000000..768edd39 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/UpdatingContext.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Nodes; + +namespace CrestApps.Core.Models; + +public sealed class UpdatingContext : HandlerContextBase +{ + public JsonNode Data { get; } + + public UpdatingContext(T model, JsonNode data) + : base(model) + { + Data = data ?? new JsonObject(); + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidatedContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidatedContext.cs new file mode 100644 index 00000000..94b5a3f1 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidatedContext.cs @@ -0,0 +1,12 @@ +namespace CrestApps.Core.Models; + +public sealed class ValidatedContext : HandlerContextBase +{ + public ValidationResultDetails Result { get; } = new(); + + public ValidatedContext(T model, ValidationResultDetails result) + : base(model) + { + Result = result ?? new(); + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidatingContext.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidatingContext.cs new file mode 100644 index 00000000..e31ee9c1 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidatingContext.cs @@ -0,0 +1,11 @@ +namespace CrestApps.Core.Models; + +public sealed class ValidatingContext : HandlerContextBase +{ + public ValidationResultDetails Result { get; } = new(); + + public ValidatingContext(T model) + : base(model) + { + } +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidationResultDetails.cs b/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidationResultDetails.cs new file mode 100644 index 00000000..25293ca4 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Models/ValidationResultDetails.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace CrestApps.Core.Models; +public class ValidationResultDetails +{ + private List _errors; + public IReadOnlyList Errors + { + get + { + return _errors ??= []; + } + } + + /// + /// Success may be altered by a handler during the validating async event. + /// + public bool Succeeded { get; set; } = true; + + public void Fail(ValidationResult error) + { + Succeeded = false; + _errors ??= []; + _errors.Add(error); + } +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalog.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalog.cs new file mode 100644 index 00000000..be7b305c --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalog.cs @@ -0,0 +1,29 @@ +namespace CrestApps.Core.Services; + +/// +/// Provides full CRUD operations for catalog entries, extending read-only access +/// with the ability to create, update, and delete entries. +/// +/// The type of catalog entry. +public interface ICatalog : IReadCatalog +{ + /// + /// Asynchronously deletes the specified entry from the catalog. + /// + /// The entry to delete. + /// if the entry was successfully deleted; otherwise, . + ValueTask DeleteAsync(T entry); + + /// + /// Asynchronously creates the specified entry in the catalog. + /// + /// The entry to create. + ValueTask CreateAsync(T entry); + + /// + /// Asynchronously updates the specified entry in the catalog. + /// + /// The entry to update. + ValueTask UpdateAsync(T entry); + +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalogEntryHandler.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalogEntryHandler.cs new file mode 100644 index 00000000..95a93dd1 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalogEntryHandler.cs @@ -0,0 +1,78 @@ +using CrestApps.Core.Models; + +namespace CrestApps.Core.Services; + +/// +/// Handles lifecycle events raised during catalog entry operations such as +/// initialization, validation, creation, update, and deletion. Implementations +/// can enrich entries, enforce business rules, or trigger side effects. +/// +/// The type of catalog entry. +public interface ICatalogEntryHandler +{ + /// + /// Called when a catalog entry is being initialized with default values. + /// + /// The context containing the entry being initialized. + Task InitializingAsync(InitializingContext context); + + /// + /// Called after a catalog entry has been initialized with default values. + /// + /// The context containing the initialized entry. + Task InitializedAsync(InitializedContext context); + + /// + /// Called after a catalog entry has been loaded from the store. + /// + /// The context containing the loaded entry. + Task LoadedAsync(LoadedContext context); + + /// + /// Called when a catalog entry is about to be validated. + /// + /// The context containing the entry to validate. + Task ValidatingAsync(ValidatingContext context); + + /// + /// Called after a catalog entry has been validated. + /// + /// The context containing the validated entry and any validation results. + Task ValidatedAsync(ValidatedContext context); + + /// + /// Called when a catalog entry is about to be deleted. + /// + /// The context containing the entry to delete. + Task DeletingAsync(DeletingContext context); + + /// + /// Called after a catalog entry has been deleted. + /// + /// The context containing the deleted entry. + Task DeletedAsync(DeletedContext context); + + /// + /// Called when a catalog entry is about to be updated. + /// + /// The context containing the entry to update. + Task UpdatingAsync(UpdatingContext context); + + /// + /// Called after a catalog entry has been updated. + /// + /// The context containing the updated entry. + Task UpdatedAsync(UpdatedContext context); + + /// + /// Called when a catalog entry is about to be created. + /// + /// The context containing the entry to create. + Task CreatingAsync(CreatingContext context); + + /// + /// Called after a catalog entry has been created. + /// + /// The context containing the created entry. + Task CreatedAsync(CreatedContext context); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalogManager.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalogManager.cs new file mode 100644 index 00000000..7fb6a065 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/ICatalogManager.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Nodes; +using CrestApps.Core.Models; + +namespace CrestApps.Core.Services; + +/// +/// Provides management-level CRUD operations for catalog entries, including +/// initialization with optional JSON data, validation, and handler invocation. +/// Extends read-only management access with write capabilities. +/// +/// The type of catalog entry. +public interface ICatalogManager : IReadCatalogManager +{ + /// + /// Asynchronously deletes the specified model from the catalog. + /// + /// The model to delete. + /// if the model was successfully deleted; otherwise, . + ValueTask DeleteAsync(T model); + + /// + /// Asynchronously creates a new model instance, optionally populating it from JSON data. + /// + /// Optional JSON data to seed the new model. + /// A newly created and initialized model instance. + ValueTask NewAsync(JsonNode data = null); + + /// + /// Asynchronously creates the specified model in the catalog. + /// + /// The model to create. + ValueTask CreateAsync(T model); + + /// + /// Asynchronously updates the specified model in the catalog, optionally merging changes from JSON data. + /// + /// The model to update. + /// Optional JSON data containing fields to merge into the model. + ValueTask UpdateAsync(T model, JsonNode data = null); + + /// + /// Asynchronously validates the specified model and returns the validation result. + /// + /// The model to validate. + /// The validation result details indicating success or failure with error messages. + ValueTask ValidateAsync(T model); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/ICloneable.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/ICloneable.cs new file mode 100644 index 00000000..946cffed --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/ICloneable.cs @@ -0,0 +1,19 @@ +namespace CrestApps.Core.Services; +/// +/// Provides a strongly-typed clone operation, creating a deep copy of the current instance. +/// Extends with a typed method. +/// +/// The type of the object being cloned. +public interface ICloneable : ICloneable +{ + /// + /// Creates a deep copy of the current instance. + /// + /// A new instance of that is a copy of this object. + new T Clone(); + /// + object ICloneable.Clone() + { + return Clone(); + } +} \ No newline at end of file diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalog.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalog.cs new file mode 100644 index 00000000..d9163763 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalog.cs @@ -0,0 +1,18 @@ +namespace CrestApps.Core.Services; + +/// +/// A catalog that supports finding entries by their unique name, +/// extending with name-based lookup for models +/// that implement . +/// +/// The type of catalog entry, which must have a property. +public interface INamedCatalog : ICatalog + where T : INameAwareModel +{ + /// + /// Asynchronously finds a catalog entry by its unique name. + /// + /// The unique name of the entry to find. + /// The matching entry, or if no entry with the specified name exists. + ValueTask FindByNameAsync(string name); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalogManager.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalogManager.cs new file mode 100644 index 00000000..4f80f9e5 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalogManager.cs @@ -0,0 +1,18 @@ +namespace CrestApps.Core.Services; + +/// +/// A catalog manager that supports finding entries by their unique name, +/// extending with name-based lookup for models +/// that implement . +/// +/// The type of catalog entry, which must have a property. +public interface INamedCatalogManager : ICatalogManager + where T : INameAwareModel +{ + /// + /// Asynchronously finds a catalog entry by its unique name. + /// + /// The unique name of the entry to find. + /// The matching entry, or if no entry with the specified name exists. + ValueTask FindByNameAsync(string name); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalog.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalog.cs new file mode 100644 index 00000000..56278736 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalog.cs @@ -0,0 +1,19 @@ +namespace CrestApps.Core.Services; + +/// +/// A catalog that supports composite lookup by both name and source, +/// extending and +/// for models that implement both and . +/// +/// The type of catalog entry. +public interface INamedSourceCatalog : INamedCatalog, ISourceCatalog + where T : INameAwareModel, ISourceAwareModel +{ + /// + /// Asynchronously retrieves a catalog entry by its unique name and source combination. + /// + /// The unique name of the entry. + /// The source or provider name of the entry. + /// The matching entry, or if not found. + ValueTask GetAsync(string name, string source); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalogManager.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalogManager.cs new file mode 100644 index 00000000..276b06cd --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalogManager.cs @@ -0,0 +1,19 @@ +namespace CrestApps.Core.Services; + +/// +/// A catalog manager that supports composite lookup by both name and source, +/// extending and +/// for models that implement both and . +/// +/// The type of catalog entry. +public interface INamedSourceCatalogManager : INamedCatalogManager, ISourceCatalogManager + where T : INameAwareModel, ISourceAwareModel +{ + /// + /// Asynchronously retrieves a catalog entry by its unique name and source combination. + /// + /// The unique name of the entry. + /// The source or provider name of the entry. + /// The matching entry, or if not found. + ValueTask GetAsync(string name, string source); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/IODataValidator.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/IODataValidator.cs new file mode 100644 index 00000000..bc1679b6 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/IODataValidator.cs @@ -0,0 +1,14 @@ +namespace CrestApps.Core.Services; + +/// +/// Validates whether the provided filter string conforms to basic OData syntax. +/// +public interface IODataValidator +{ + /// + /// Determines whether the specified filter string is a valid OData filter expression. + /// + /// The OData filter string to validate. + /// if the filter conforms to basic OData syntax; otherwise, . + bool IsValidFilter(string filter); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/IReadCatalog.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/IReadCatalog.cs new file mode 100644 index 00000000..1f4652e5 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/IReadCatalog.cs @@ -0,0 +1,42 @@ +using CrestApps.Core.Models; + +namespace CrestApps.Core.Services; + +/// +/// Provides read-only access to catalog entries, supporting retrieval by ID, +/// bulk retrieval, and paginated queries. +/// +/// The type of catalog entry. +public interface IReadCatalog +{ + /// + /// Asynchronously finds a catalog entry by its unique identifier. + /// + /// The unique identifier of the entry. + /// The matching entry, or if not found. + ValueTask FindByIdAsync(string id); + + /// + /// Asynchronously retrieves all entries in the catalog. + /// + /// A read-only collection of all catalog entries. + ValueTask> GetAllAsync(); + + /// + /// Asynchronously retrieves catalog entries matching the specified identifiers. + /// + /// The identifiers of the entries to retrieve. + /// A read-only collection of matching entries. + ValueTask> GetAsync(IEnumerable ids); + + /// + /// Asynchronously retrieves a paginated subset of catalog entries using the specified query context. + /// + /// The query context type used to filter and order results. + /// The one-based page number to retrieve. + /// The number of entries per page. + /// The query context used to filter and order results. + /// A page result containing the entries and total count. + ValueTask> PageAsync(int page, int pageSize, TQuery context) + where TQuery : QueryContext; +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/IReadCatalogManager.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/IReadCatalogManager.cs new file mode 100644 index 00000000..4a17ac9b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/IReadCatalogManager.cs @@ -0,0 +1,35 @@ +using CrestApps.Core.Models; + +namespace CrestApps.Core.Services; + +/// +/// Provides read-only management access to catalog entries with handler invocation, +/// supporting retrieval by ID, bulk listing, and paginated queries. +/// +/// The type of catalog entry. +public interface IReadCatalogManager +{ + /// + /// Asynchronously finds a catalog entry by its unique identifier. + /// + /// The unique identifier of the entry. + /// The matching entry, or if not found. + ValueTask FindByIdAsync(string id); + + /// + /// Asynchronously retrieves all entries in the catalog. + /// + /// An enumerable of all catalog entries. + ValueTask> GetAllAsync(); + + /// + /// Asynchronously retrieves a paginated subset of catalog entries using the specified query context. + /// + /// The query context type used to filter and order results. + /// The one-based page number to retrieve. + /// The number of entries per page. + /// The query context used to filter and order results. + /// A page result containing the entries and total count. + ValueTask> PageAsync(int page, int pageSize, TQuery context) + where TQuery : QueryContext; +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/ISourceCatalog.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/ISourceCatalog.cs new file mode 100644 index 00000000..c7dd8a50 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/ISourceCatalog.cs @@ -0,0 +1,17 @@ +namespace CrestApps.Core.Services; + +/// +/// A catalog that supports filtering entries by their source or provider name, +/// extending for models that implement . +/// +/// The type of catalog entry, which must have a property. +public interface ISourceCatalog : ICatalog + where T : ISourceAwareModel +{ + /// + /// Asynchronously retrieves all catalog entries belonging to the specified source. + /// + /// The source or provider name to filter by. + /// A read-only collection of entries matching the specified source. + ValueTask> GetAsync(string source); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/ISourceCatalogManager.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/ISourceCatalogManager.cs new file mode 100644 index 00000000..574d981e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/ISourceCatalogManager.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Nodes; + +namespace CrestApps.Core.Services; + +/// +/// A catalog manager that supports source-scoped creation and filtering, +/// extending for models that implement . +/// +/// The type of catalog entry, which must have a property. +public interface ISourceCatalogManager : ICatalogManager + where T : ISourceAwareModel +{ + /// + /// Asynchronously creates a new model instance pre-assigned to the specified source, + /// optionally populating it from JSON data. + /// + /// The source or provider name to assign to the new model. + /// Optional JSON data to seed the new model. + /// A newly created and initialized model instance assigned to the specified source. + ValueTask NewAsync(string source, JsonNode data = null); + + /// + /// Asynchronously retrieves all catalog entries belonging to the specified source. + /// + /// The source or provider name to filter by. + /// An enumerable of entries matching the specified source. + ValueTask> GetAsync(string source); + + /// + /// Asynchronously finds all catalog entries that belong to the specified source. + /// + /// The source or provider name to search for. + /// An enumerable of entries matching the specified source. + ValueTask> FindBySourceAsync(string source); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/IStoreCommitter.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/IStoreCommitter.cs new file mode 100644 index 00000000..1abe37ac --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/IStoreCommitter.cs @@ -0,0 +1,16 @@ +namespace CrestApps.Core.Services; + +/// +/// Represents a commit boundary for the underlying data store. +/// Implementations flush all staged writes (e.g., ISession.SaveChangesAsync for YesSql) +/// to durable storage. The framework registers this interface for each first-party store +/// package and calls it automatically via , +/// , and +/// . SignalR hub implementations that +/// perform fire-and-forget operations must call explicitly +/// from within the async work lambda. +/// +public interface IStoreCommitter +{ + ValueTask CommitAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.Abstractions/UniqueId.cs b/src/Abstractions/CrestApps.Core.Abstractions/UniqueId.cs new file mode 100644 index 00000000..1a8f4c22 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Abstractions/UniqueId.cs @@ -0,0 +1,71 @@ +namespace CrestApps.Core; + +/// +/// Generates and validates unique 26-character base32-encoded identifiers. +/// +public static class UniqueId +{ + private const int _idLength = 26; + + // Some confusing chars are ignored: http://www.crockford.com/wrmg/base32.html + private static readonly char[] _encode32Chars = "0123456789abcdefghjkmnpqrstvwxyz".ToCharArray(); + + private static readonly HashSet _allowedChars = [.. "0123456789abcdefghjkmnpqrstvwxyz"]; + /// + /// Generates a new unique 26-character identifier. + /// + public static string GenerateId() + { + var guid = Guid.NewGuid().ToByteArray(); + + return string.Create(26, guid, (buffer, guid) => + { + var hs = BitConverter.ToInt64(guid, 0); + var ls = BitConverter.ToInt64(guid, 8); + + // Using a local copy prevents additional bound checks by the JIT. + var encode32Chars = _encode32Chars; + + // A char array allows a long as the indexer, so without any cast. + buffer[0] = encode32Chars[(hs >> 60) & 31]; + buffer[1] = encode32Chars[(hs >> 55) & 31]; + buffer[2] = encode32Chars[(hs >> 50) & 31]; + buffer[3] = encode32Chars[(hs >> 45) & 31]; + buffer[4] = encode32Chars[(hs >> 40) & 31]; + buffer[5] = encode32Chars[(hs >> 35) & 31]; + buffer[6] = encode32Chars[(hs >> 30) & 31]; + buffer[7] = encode32Chars[(hs >> 25) & 31]; + buffer[8] = encode32Chars[(hs >> 20) & 31]; + buffer[9] = encode32Chars[(hs >> 15) & 31]; + buffer[10] = encode32Chars[(hs >> 10) & 31]; + buffer[11] = encode32Chars[(hs >> 5) & 31]; + buffer[12] = encode32Chars[hs & 31]; + + buffer[13] = encode32Chars[(ls >> 60) & 31]; + buffer[14] = encode32Chars[(ls >> 55) & 31]; + buffer[15] = encode32Chars[(ls >> 50) & 31]; + buffer[16] = encode32Chars[(ls >> 45) & 31]; + buffer[17] = encode32Chars[(ls >> 40) & 31]; + buffer[18] = encode32Chars[(ls >> 35) & 31]; + buffer[19] = encode32Chars[(ls >> 30) & 31]; + buffer[20] = encode32Chars[(ls >> 25) & 31]; + buffer[21] = encode32Chars[(ls >> 20) & 31]; + buffer[22] = encode32Chars[(ls >> 15) & 31]; + buffer[23] = encode32Chars[(ls >> 10) & 31]; + buffer[24] = encode32Chars[(ls >> 5) & 31]; + buffer[25] = encode32Chars[ls & 31]; + }); + } + /// + /// Validates whether the given string is a valid 26-character base32 identifier. + /// + public static bool IsValid(string id) + { + if (id is null || id.Length != _idLength) + { + return false; + } + + return id.All(_allowedChars.Contains); + } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/CrestApps.Core.Infrastructure.Abstractions.csproj b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/CrestApps.Core.Infrastructure.Abstractions.csproj new file mode 100644 index 00000000..2179a1c9 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/CrestApps.Core.Infrastructure.Abstractions.csproj @@ -0,0 +1,18 @@ + + + + CrestApps.Core.Infrastructure + CrestApps Infrastructure Abstractions + + $(CrestAppsDescription) + + Core infrastructure abstractions for indexing, search, and data source services. Framework-independent and reusable outside the AI layer. + + $(PackageTags) Infrastructure Abstractions Indexing Search + + + + + + + diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/DataSources/IDataSourceContentManager.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/DataSources/IDataSourceContentManager.cs new file mode 100644 index 00000000..af9457c6 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/DataSources/IDataSourceContentManager.cs @@ -0,0 +1,41 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; + +namespace CrestApps.Core.Infrastructure.Indexing.DataSources; + +/// +/// Manages document chunks within a data source index, supporting similarity search +/// and bulk deletion. Implementations are registered as keyed services using the +/// index provider name as the key. +/// +public interface IDataSourceContentManager +{ + /// + /// Searches for document chunks that are similar to the provided embedding vector. + /// + /// The index profile describing the target index. + /// The embedding vector to search against. + /// The identifier of the data source to search within. + /// The maximum number of results to return. + /// An optional filter expression to narrow the search. + /// A token to cancel the operation. + /// An enumerable of search results ranked by similarity. + Task> SearchAsync( + IIndexProfileInfo indexProfile, + float[] embedding, + string dataSourceId, + int topN, + string filter = null, + CancellationToken cancellationToken = default); + + /// + /// Deletes all document chunks belonging to the specified data source from the index. + /// + /// The index profile describing the target index. + /// The identifier of the data source whose chunks should be deleted. + /// A token to cancel the operation. + /// The number of document chunks deleted. + Task DeleteByDataSourceIdAsync( + IIndexProfileInfo indexProfile, + string dataSourceId, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/DataSources/IDataSourceDocumentReader.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/DataSources/IDataSourceDocumentReader.cs new file mode 100644 index 00000000..560e8451 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/DataSources/IDataSourceDocumentReader.cs @@ -0,0 +1,30 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; + +namespace CrestApps.Core.Infrastructure.Indexing.DataSources; + +/// +/// Reads documents from a source index. Implementations are keyed by provider name. +/// +public interface IDataSourceDocumentReader +{ + /// + /// Reads documents from the specified source index in batches. + /// + IAsyncEnumerable> ReadAsync( + IIndexProfileInfo indexProfile, + string keyFieldName, + string titleFieldName, + string contentFieldName, + CancellationToken cancellationToken = default); + + /// + /// Reads specific documents from the source index by their native document IDs. + /// + IAsyncEnumerable> ReadByIdsAsync( + IIndexProfileInfo indexProfile, + IEnumerable documentIds, + string keyFieldName, + string titleFieldName, + string contentFieldName, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IIndexProfileHandler.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IIndexProfileHandler.cs new file mode 100644 index 00000000..f5878126 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IIndexProfileHandler.cs @@ -0,0 +1,23 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; +using CrestApps.Core.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.Infrastructure.Indexing; + +public interface IIndexProfileHandler : ICatalogEntryHandler +{ + ValueTask ValidateAsync( + SearchIndexProfile indexProfile, + ValidationResultDetails result, + CancellationToken cancellationToken = default); + + ValueTask> GetFieldsAsync( + SearchIndexProfile indexProfile, + CancellationToken cancellationToken = default); + + Task SynchronizedAsync(SearchIndexProfile indexProfile, CancellationToken cancellationToken = default); + + Task ResetAsync(SearchIndexProfile indexProfile, CancellationToken cancellationToken = default); + + Task DeletingAsync(SearchIndexProfile indexProfile, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IIndexProfileInfo.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IIndexProfileInfo.cs new file mode 100644 index 00000000..e5a4699e --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IIndexProfileInfo.cs @@ -0,0 +1,28 @@ +namespace CrestApps.Core.Infrastructure.Indexing; + +/// +/// Provides index profile information for data source and vector search operations. +/// This abstraction decouples the AI framework from specific indexing implementations. +/// +public interface IIndexProfileInfo +{ + /// + /// Gets the unique identifier for the index profile. + /// + string IndexProfileId { get; } + + /// + /// Gets the name of the index. + /// + string IndexName { get; } + + /// + /// Gets the name of the index provider (e.g., "Elasticsearch", "AzureAISearch"). + /// + string ProviderName { get; } + + /// + /// Gets the full name of the index including any tenant prefix. + /// + string IndexFullName { get; } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IODataFilterTranslator.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IODataFilterTranslator.cs new file mode 100644 index 00000000..d32779e3 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IODataFilterTranslator.cs @@ -0,0 +1,17 @@ +namespace CrestApps.Core.Infrastructure.Indexing; + +/// +/// Translates OData filter expressions into provider-specific filter queries +/// for use against the knowledge base index's filter fields. +/// Implementations are registered as keyed services using the provider name. +/// +public interface IODataFilterTranslator +{ + /// + /// Translates an OData filter expression into a provider-specific filter string. + /// The translated filter targets the "filters." prefixed fields in the knowledge base index. + /// + /// The OData filter expression (e.g., "status eq 'active' and category eq 'docs'"). + /// A provider-specific filter string, or null if the filter could not be translated. + string Translate(string odataFilter); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchDocumentManager.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchDocumentManager.cs new file mode 100644 index 00000000..2e942d85 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchDocumentManager.cs @@ -0,0 +1,34 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; + +namespace CrestApps.Core.Infrastructure.Indexing; + +/// +/// Manages documents within a search index (add, update, delete). +/// Implementations are registered as keyed services using the provider name as the key. +/// +public interface ISearchDocumentManager +{ + /// + /// Adds or updates documents in the specified index. + /// + /// The index profile describing the target index. + /// The documents to add or update. + /// A cancellation token. + /// if the operation succeeded; otherwise, . + Task AddOrUpdateAsync(IIndexProfileInfo profile, IReadOnlyCollection documents, CancellationToken cancellationToken = default); + + /// + /// Deletes specific documents from the index by their IDs. + /// + /// The index profile describing the target index. + /// The IDs of the documents to delete. + /// A cancellation token. + Task DeleteAsync(IIndexProfileInfo profile, IEnumerable documentIds, CancellationToken cancellationToken = default); + + /// + /// Deletes all documents from the specified index. + /// + /// The index profile describing the target index. + /// A cancellation token. + Task DeleteAllAsync(IIndexProfileInfo profile, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexManager.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexManager.cs new file mode 100644 index 00000000..9057c5e2 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexManager.cs @@ -0,0 +1,40 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; + +namespace CrestApps.Core.Infrastructure.Indexing; + +/// +/// Manages the lifecycle of search indexes in a search backend (e.g., Elasticsearch, Azure AI Search). +/// Implementations are registered as keyed services using the provider name as the key. +/// +public interface ISearchIndexManager +{ + /// + /// Checks whether the specified index exists. + /// + /// The index profile. + /// A cancellation token. + /// if the index exists; otherwise, . + Task ExistsAsync(IIndexProfileInfo profile, CancellationToken cancellationToken = default); + + /// + /// Composes the full remote index name for the supplied profile. + /// + /// The index profile. + /// The fully qualified index name. + string ComposeIndexFullName(IIndexProfileInfo profile); + + /// + /// Creates a new search index with the specified fields. + /// + /// The index profile describing the index. + /// The field definitions for the index schema. + /// A cancellation token. + Task CreateAsync(IIndexProfileInfo profile, IReadOnlyCollection fields, CancellationToken cancellationToken = default); + + /// + /// Deletes the specified search index. + /// + /// The index profile. + /// A cancellation token. + Task DeleteAsync(IIndexProfileInfo profile, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexProfileManager.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexProfileManager.cs new file mode 100644 index 00000000..0c431fc8 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexProfileManager.cs @@ -0,0 +1,19 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.Infrastructure.Indexing; + +public interface ISearchIndexProfileManager : ICatalogManager +{ + ValueTask FindByNameAsync(string name); + + Task> GetByTypeAsync(string type); + + ValueTask> GetFieldsAsync( + SearchIndexProfile profile, + CancellationToken cancellationToken = default); + + Task SynchronizeAsync(SearchIndexProfile profile, CancellationToken cancellationToken = default); + + Task ResetAsync(SearchIndexProfile profile, CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexProfileStore.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexProfileStore.cs new file mode 100644 index 00000000..aabb467c --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/ISearchIndexProfileStore.cs @@ -0,0 +1,20 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.Infrastructure.Indexing; + +/// +/// Store for managing records. +/// +public interface ISearchIndexProfileStore : ICatalog, INamedCatalog +{ + /// + /// Gets all index profiles of the specified type (e.g., "AIDocuments", "DataSourceIndex", "AIMemory"). + + /// + /// Gets all index profiles of the specified type (e.g., "AIDocuments", "DataSourceIndex", "AIMemory"). + /// + /// The index profile type to filter by. + /// A read-only collection of matching index profiles. + Task> GetByTypeAsync(string type); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IVectorSearchService.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IVectorSearchService.cs new file mode 100644 index 00000000..1d9e0077 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IVectorSearchService.cs @@ -0,0 +1,28 @@ +using CrestApps.Core.Infrastructure.Indexing.Models; + +namespace CrestApps.Core.Infrastructure.Indexing; + +/// +/// Interface for searching document embeddings in various index providers. +/// Implementations should be registered as keyed services using the provider name. +/// +public interface IVectorSearchService +{ + /// + /// Searches for document chunks that are similar to the provided embedding vector. + /// + /// The index profile describing the target index. + /// The embedding vector to search against. + /// The reference entity identifier to scope the search. + /// The type of the reference entity. + /// The maximum number of results to return. + /// A token to cancel the operation. + /// An enumerable of document chunk search results ranked by similarity. + Task> SearchAsync( + IIndexProfileInfo indexProfile, + float[] embedding, + string referenceId, + string referenceType, + int topN, + CancellationToken cancellationToken = default); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileSourceDescriptor.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileSourceDescriptor.cs new file mode 100644 index 00000000..8bd5c0fd --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileSourceDescriptor.cs @@ -0,0 +1,14 @@ +namespace CrestApps.Core.Infrastructure.Indexing; + +public sealed class IndexProfileSourceDescriptor +{ + public string ProviderName { get; set; } + + public string ProviderDisplayName { get; set; } + + public string Type { get; set; } + + public string DisplayName { get; set; } + + public string Description { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileSourceOptions.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileSourceOptions.cs new file mode 100644 index 00000000..6b2db24b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileSourceOptions.cs @@ -0,0 +1,37 @@ +namespace CrestApps.Core.Infrastructure.Indexing; + +public sealed class IndexProfileSourceOptions +{ + public List Sources { get; } = []; + + public void AddOrUpdate( + string providerName, + string providerDisplayName, + string type, + Action configure = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(providerName); + ArgumentException.ThrowIfNullOrWhiteSpace(providerDisplayName); + ArgumentException.ThrowIfNullOrWhiteSpace(type); + + var descriptor = Sources.FirstOrDefault(source => + string.Equals(source.ProviderName, providerName, StringComparison.OrdinalIgnoreCase) && + string.Equals(source.Type, type, StringComparison.OrdinalIgnoreCase)); + + descriptor ??= new IndexProfileSourceDescriptor + { + ProviderName = providerName, + ProviderDisplayName = providerDisplayName, + Type = type, + DisplayName = type, + Description = type, + }; + + configure?.Invoke(descriptor); + + if (!Sources.Contains(descriptor)) + { + Sources.Add(descriptor); + } + } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileTypes.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileTypes.cs new file mode 100644 index 00000000..0c94d037 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/IndexProfileTypes.cs @@ -0,0 +1,27 @@ +namespace CrestApps.Core.Infrastructure.Indexing; + +/// +/// Well-known index profile type identifiers. +/// +public static class IndexProfileTypes +{ + /// + /// Index type for AI document chunks (uploaded files chunked and embedded). + /// + public const string AIDocuments = "AIDocuments"; + + /// + /// Index type for data source knowledge base entries. + /// + public const string DataSource = "DataSourceIndex"; + + /// + /// Index type for AI memory entries. + /// + public const string AIMemory = "AIMemory"; + + /// + /// Index type for custom article records. + /// + public const string Articles = "Articles"; +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/ChatInteractionDocumentChunk.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/ChatInteractionDocumentChunk.cs new file mode 100644 index 00000000..6cd254d0 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/ChatInteractionDocumentChunk.cs @@ -0,0 +1,23 @@ +namespace CrestApps.Core.Infrastructure.Indexing.Models; + +/// +/// Represents a document chunk for embedding and indexing. +/// Each chunk contains a paragraph or section of the original document. +/// +public sealed class ChatInteractionDocumentChunk +{ + /// + /// Gets or sets the text content of this chunk. + /// + public string Text { get; set; } + + /// + /// Gets or sets the embedding vector for this chunk. + /// + public float[] Embedding { get; set; } + + /// + /// Gets or sets the chunk index within the document. + /// + public int Index { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/DataSourceSearchResult.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/DataSourceSearchResult.cs new file mode 100644 index 00000000..3a2fe98d --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/DataSourceSearchResult.cs @@ -0,0 +1,38 @@ +namespace CrestApps.Core.Infrastructure.Indexing.Models; + +/// +/// Represents a search result from a data source knowledge base index. +/// +public sealed class DataSourceSearchResult +{ + /// + /// Gets or sets the reference ID of the source document. + /// + public string ReferenceId { get; set; } + + /// + /// Gets or sets the title of the source document. + /// + public string Title { get; set; } + + /// + /// Gets or sets the text content of the matching chunk. + /// + public string Content { get; set; } + + /// + /// Gets or sets the chunk index within the document. + /// + public int ChunkIndex { get; set; } + + /// + /// Gets or sets the reference type that identifies the kind of source + /// (e.g., "Content" for Orchard Core content items, or the source index profile type). + /// + public string ReferenceType { get; set; } + + /// + /// Gets or sets the similarity score. + /// + public float Score { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/DocumentChunkSearchResult.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/DocumentChunkSearchResult.cs new file mode 100644 index 00000000..9016ffc3 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/DocumentChunkSearchResult.cs @@ -0,0 +1,27 @@ +namespace CrestApps.Core.Infrastructure.Indexing.Models; + +/// +/// Represents a search result from the document index. +/// +public sealed class DocumentChunkSearchResult +{ + /// + /// Gets or sets the document chunk. + /// + public ChatInteractionDocumentChunk Chunk { get; set; } + + /// + /// Gets or sets the unique key that identifies the source document. + /// + public string DocumentKey { get; set; } + + /// + /// Gets or sets the original file name of the source document. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the similarity score. + /// + public float Score { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/IndexDocument.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/IndexDocument.cs new file mode 100644 index 00000000..e6003239 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/IndexDocument.cs @@ -0,0 +1,18 @@ +namespace CrestApps.Core.Infrastructure.Indexing.Models; + +/// +/// Represents a document to be indexed in a search backend. +/// +public sealed class IndexDocument +{ + /// + /// Gets or sets the unique identifier for this document. + /// + public string Id { get; set; } + + /// + /// Gets or sets the field values for this document. + /// Keys are field names; values are the corresponding data. + /// + public IDictionary Fields { get; set; } = new Dictionary(); +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchFieldType.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchFieldType.cs new file mode 100644 index 00000000..e420c8e1 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchFieldType.cs @@ -0,0 +1,37 @@ +namespace CrestApps.Core.Infrastructure.Indexing.Models; + +/// +/// Defines the data type of a field in a search index. +/// +public enum SearchFieldType +{ + /// + /// Full-text searchable content. + /// + Text, + + /// + /// Exact-match keyword (not tokenized). + /// + Keyword, + + /// + /// Integer numeric value. + /// + Integer, + + /// + /// Floating-point numeric value. + /// + Float, + + /// + /// Date/time value. + /// + DateTime, + + /// + /// Dense vector for similarity search. + /// + Vector, +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchIndexField.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchIndexField.cs new file mode 100644 index 00000000..fa3e6c7b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchIndexField.cs @@ -0,0 +1,39 @@ +namespace CrestApps.Core.Infrastructure.Indexing.Models; + +/// +/// Describes a field in a search index, including its type, whether it is the key, +/// and vector-specific settings. +/// +public sealed class SearchIndexField +{ + /// + /// Gets or sets the field name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the field data type. + /// + public SearchFieldType FieldType { get; set; } + + /// + /// Gets or sets whether this field is the document key. + /// + public bool IsKey { get; set; } + + /// + /// Gets or sets whether this field supports filtering. + /// + public bool IsFilterable { get; set; } + + /// + /// Gets or sets whether this field supports full-text search. + /// + public bool IsSearchable { get; set; } + + /// + /// Gets or sets the number of dimensions for a vector field. + /// Only applicable when is . + /// + public int? VectorDimensions { get; set; } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchIndexProfile.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchIndexProfile.cs new file mode 100644 index 00000000..02433e8b --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SearchIndexProfile.cs @@ -0,0 +1,82 @@ +using CrestApps.Core.Models; +using CrestApps.Core.Services; + +namespace CrestApps.Core.Infrastructure.Indexing.Models; + +/// +/// Represents an index profile that defines how a search index is configured +/// and which provider manages it (e.g., Elasticsearch, Azure AI Search). +/// +public sealed class SearchIndexProfile : CatalogItem, IIndexProfileInfo, INameAwareModel, IDisplayTextAwareModel, ICloneable +{ + /// + /// Gets or sets the unique name for this index profile. + /// + public string Name { get; set; } + + /// + /// Gets or sets the display text for this index profile. + /// + public string DisplayText { get; set; } + + /// + /// Gets or sets the name of the underlying search index. + /// + public string IndexName { get; set; } + + /// + /// Gets or sets the search provider name (e.g., "Elasticsearch", "AzureAISearch"). + /// + public string ProviderName { get; set; } + + /// + /// Gets or sets the fully qualified index name, which may include a tenant or application prefix. + /// + public string IndexFullName { get; set; } + + /// + /// Gets or sets the type of data stored in this index (e.g., "AIDocuments", "DataSourceIndex", "AIMemory"). + /// + public string Type { get; set; } + + /// + /// Gets or sets the deployment ID used for generating embeddings. + /// + public string EmbeddingDeploymentId { get; set; } + + /// + /// Gets or sets the date and time when this index profile was created. + /// + public DateTime CreatedUtc { get; set; } + + /// + /// Gets or sets the owner identifier. + /// + public string OwnerId { get; set; } + + /// + /// Gets or sets the author name. + /// + public string Author { get; set; } + + string IIndexProfileInfo.IndexProfileId => ItemId; + + public SearchIndexProfile Clone() + { + return new SearchIndexProfile + { + ItemId = ItemId, + Name = Name, + DisplayText = DisplayText, + IndexName = IndexName, + ProviderName = ProviderName, + IndexFullName = IndexFullName, + Type = Type, + EmbeddingDeploymentId = EmbeddingDeploymentId, + CreatedUtc = CreatedUtc, + OwnerId = OwnerId, + Author = Author, + Properties = Properties.Clone(), + }; + } +} diff --git a/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SourceDocument.cs b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SourceDocument.cs new file mode 100644 index 00000000..cd7d0a43 --- /dev/null +++ b/src/Abstractions/CrestApps.Core.Infrastructure.Abstractions/Indexing/Models/SourceDocument.cs @@ -0,0 +1,23 @@ +namespace CrestApps.Core.Infrastructure.Indexing.Models; + +/// +/// Represents a document read from a source index with extracted title, content, and all source fields. +/// +public sealed class SourceDocument +{ + /// + /// Gets or sets the document title. + /// + public string Title { get; set; } + + /// + /// Gets or sets the document content text. + /// + public string Content { get; set; } + + /// + /// Gets or sets all fields from the source document. + /// Used for populating filter fields in the knowledge base index. + /// + public Dictionary Fields { get; set; } +} diff --git a/src/CrestApps.Core.Docs/.gitignore b/src/CrestApps.Core.Docs/.gitignore new file mode 100644 index 00000000..b2d6de30 --- /dev/null +++ b/src/CrestApps.Core.Docs/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/src/CrestApps.Core.Docs/CrestApps.Core.Docs.csproj b/src/CrestApps.Core.Docs/CrestApps.Core.Docs.csproj new file mode 100644 index 00000000..f4e56132 --- /dev/null +++ b/src/CrestApps.Core.Docs/CrestApps.Core.Docs.csproj @@ -0,0 +1,19 @@ + + + + $(CommonTargetFrameworks) + false + + + + + + + + + + + + + + diff --git a/src/CrestApps.Core.Docs/README.md b/src/CrestApps.Core.Docs/README.md new file mode 100644 index 00000000..2d213b8c --- /dev/null +++ b/src/CrestApps.Core.Docs/README.md @@ -0,0 +1,21 @@ +# CrestApps.Core Documentation + +Documentation site for the [CrestApps.Core](https://github.com/CrestApps/CrestApps.Core) repository, built with [Docusaurus 3.9](https://docusaurus.io/). + +**Live site:** [core.crestapps.com](https://core.crestapps.com) + +## Local Development + +```bash +cd src/CrestApps.Core.Docs +npm install +npm start +``` + +## Build + +```bash +npm run build +``` + +This site contains the framework-only documentation for `CrestApps.Core`. diff --git a/src/CrestApps.Core.Docs/docs/a2a/client.md b/src/CrestApps.Core.Docs/docs/a2a/client.md new file mode 100644 index 00000000..e576fa6f --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/a2a/client.md @@ -0,0 +1,524 @@ +--- +sidebar_label: A2A Client +sidebar_position: 2 +title: A2A Client +description: Discover and invoke remote AI agents using the A2A protocol client — connection management, tool registry integration, authentication, and built-in discovery tools. +--- + +# A2A Client + +> Discover and invoke remote AI agents by registering A2A connections, fetching their Agent Cards, and exposing their skills as tools in the orchestrator. + +## Quick Start + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration() + .AddCoreAIA2AClient(); +``` + +This single call registers everything needed to consume remote A2A agents: HTTP infrastructure, agent card caching, tool registry integration, authentication services, and three built-in discovery tools. + +## Problem & Solution + +Your AI application needs to delegate tasks to agents running in other applications — a translation service, a code-review agent, a document-summarization agent. Each remote agent has its own AI model, tools, and reasoning. You need a way to: + +- **Discover** what remote agents can do (without hardcoding their capabilities) +- **Invoke** remote agents as if they were local tools +- **Authenticate** with each remote host (API keys, OAuth2, certificates) +- **Cache** agent metadata for performance + +The A2A client solves all of this. It fetches Agent Cards from remote hosts, converts each advertised skill into a tool registry entry, and proxies tool calls to the remote agent transparently. + +## Services Registered by `AddCoreAIA2AClient()` + +| Service | Implementation | Lifetime | Purpose | +|---------|---------------|----------|---------| +| `HttpClient` | via `IHttpClientFactory` | — | HTTP communication with remote hosts | +| `IMemoryCache` | — | Singleton | Caching infrastructure for agent cards and OAuth2 tokens | +| `IHttpContextAccessor` | `HttpContextAccessor` | Singleton | Access to the current HTTP context for scoped service resolution | +| `IAICompletionContextBuilderHandler` | `A2AAICompletionContextBuilderHandler` | Scoped | Copies A2A connection IDs from the AI profile into the completion context | +| `IToolRegistryProvider` | `A2AToolRegistryProvider` | Scoped | Discovers remote agent skills and exposes them as tool entries | +| `IA2AAgentCardCacheService` | `DefaultA2AAgentCardCacheService` | Singleton | Fetches and caches Agent Cards from remote hosts (15-minute TTL) | +| `IA2AConnectionAuthService` | `DefaultA2AConnectionAuthService` | Scoped | Builds HTTP authentication headers for each connection | + +### Built-in Tools + +Three system tools are registered automatically: + +| Tool Name | Class | Purpose | +|-----------|-------|---------| +| `listAvailableAgents` | `ListAvailableAgentsFunction` | Lists all available agents — both local AI profiles and remote A2A agents | +| `findAgentForTask` | `FindAgentForTaskFunction` | Finds the best agents for a given task using keyword matching | +| `findToolsForTask` | `FindToolsForTaskFunction` | Discovers tools (including remote agent skills) relevant to a task | + +## How It All Fits Together + +```text +┌─────────────────────────────────────────────────────────────────┐ +│ Your Application │ +│ │ +│ 1. AI Profile has A2AConnectionIds = ["conn-abc", "conn-xyz"] │ +│ │ │ +│ 2. A2AAICompletionContextBuilderHandler copies connection IDs │ +│ into AICompletionContext.A2AConnectionIds │ +│ │ │ +│ 3. A2AToolRegistryProvider.GetToolsAsync() runs: │ +│ ┌───────────────────▼──────────────────┐ │ +│ │ For each connection ID: │ │ +│ │ a. Load A2AConnection from store │ │ +│ │ b. Fetch Agent Card (cached 15 min) │ │ +│ │ c. For each skill on the card: │ │ +│ │ → Create ToolRegistryEntry │ │ +│ │ Id: "a2a:{connId}:{skillName}" │ │ +│ │ Source: A2AAgent │ │ +│ │ Factory: → A2AAgentProxyTool │ │ +│ └──────────────────────────────────────┘ │ +│ │ │ +│ 4. AI model sees remote skills as invokable tools │ +│ │ │ +│ 5. Model calls a tool → A2AAgentProxyTool executes: │ +│ a. Load connection + auth metadata │ +│ b. Configure HttpClient with auth headers │ +│ c. Send AgentMessage to remote endpoint │ +│ d. Extract text from response │ +│ e. Return text to the AI model │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Connection Management + +### The `A2AConnection` Model + +Every remote A2A host is represented by an `A2AConnection`: + +```csharp +public sealed class A2AConnection : CatalogItem, IDisplayTextAwareModel +{ + // Human-readable name for the connection (e.g., "Legal Review Agent") + public string DisplayText { get; set; } + + // The remote A2A host's base URL (e.g., "https://agents.example.com/a2a") + public string Endpoint { get; set; } + + // When the connection was created + public DateTime CreatedUtc { get; set; } + + // Who created the connection + public string Author { get; set; } + + // Owner user ID + public string OwnerId { get; set; } +} +``` + +`A2AConnection` extends `CatalogItem`, which means it supports the `Properties` dictionary for extensible metadata. Authentication details are stored as an `A2AConnectionMetadata` object in this dictionary. + +### Implementing `ICatalog` + +The framework defines the `A2AConnection` model but does **not** include a built-in store. You must implement `ICatalog` to persist connections in your data store: + +```csharp +public sealed class MyA2AConnectionStore : ICatalog +{ + private readonly IDbConnection _db; + + public MyA2AConnectionStore(IDbConnection db) + { + _db = db; + } + + public Task FindByIdAsync(string id) + { + // Load from your database + } + + public Task> GetAllAsync() + { + // Return all configured connections + } + + // ... other CRUD methods +} + +// Register in DI +builder.Services.AddScoped, MyA2AConnectionStore>(); +``` + +::: + +### Assigning Connections to AI Profiles + +Connections are linked to AI profiles via `AIProfileA2AMetadata`: + +```csharp +public sealed class AIProfileA2AMetadata +{ + // The IDs of A2A connections available to this profile + public string[] ConnectionIds { get; set; } +} +``` + +When the orchestrator builds the completion context, `A2AAICompletionContextBuilderHandler` reads these IDs from the profile and sets them on `AICompletionContext.A2AConnectionIds`. This tells `A2AToolRegistryProvider` which remote hosts to query for tools. + +```csharp +// How the handler works internally: +internal sealed class A2AAICompletionContextBuilderHandler : IAICompletionContextBuilderHandler +{ + public Task BuildingAsync(AICompletionContextBuildingContext context) + { + if (context.Resource is AIProfile profile && + profile.TryGet(out var a2aMetadata)) + { + context.Context.A2AConnectionIds = a2aMetadata.ConnectionIds; + } + + return Task.CompletedTask; + } +} +``` + +## Agent Card Discovery & Caching + +### What Is an Agent Card? + +An Agent Card is a JSON document published by an A2A host at a well-known URL (typically `/.well-known/agent.json`). It describes: + +- The agent's name and description +- A list of **skills** (capabilities the agent can perform) +- Each skill's ID, name, description, and tags +- The endpoint URL for sending messages + +### `IA2AAgentCardCacheService` + +The framework caches Agent Cards in memory to avoid fetching them on every request: + +```csharp +public interface IA2AAgentCardCacheService +{ + /// Fetches the Agent Card for a connection, using a cached value if available. + Task GetAgentCardAsync( + string connectionId, + A2AConnection connection, + CancellationToken cancellationToken = default); + + /// Removes the cached Agent Card for a connection. + void Invalidate(string connectionId); +} +``` + +The default implementation (`DefaultA2AAgentCardCacheService`) caches cards for **15 minutes** using `IMemoryCache`. It: + +1. Checks the cache using key `A2AAgentCard:{connectionId}` +2. On cache miss, creates an `HttpClient` and configures it with authentication headers +3. Uses `A2ACardResolver` to fetch the Agent Card from the remote host +4. Caches the result and returns it + +To customize caching behavior (e.g., use distributed cache, change TTL), register your own implementation: + +```csharp +builder.Services.AddSingleton(); +``` + +## Tool Registry Integration + +### How Remote Agents Become Tools + +`A2AToolRegistryProvider` implements `IToolRegistryProvider` and is called by the orchestrator when building the tool set for a completion request. + +For each connection ID in `AICompletionContext.A2AConnectionIds`: + +1. **Load** the `A2AConnection` from the catalog +2. **Fetch** the Agent Card (cached) +3. **Iterate** skills on the Agent Card +4. **Create** a `ToolRegistryEntry` for each skill: + +```csharp +new ToolRegistryEntry +{ + Id = $"a2a:{connectionId}:{skillName}", // Unique tool ID + Name = skillName, // Sanitized skill name + Description = skill.Description, // Shown to the AI model + Source = ToolRegistryEntrySource.A2AAgent, // Identifies this as an A2A tool + SourceId = connectionId, // Links back to the connection + CreateAsync = _ => new A2AAgentProxyTool(...) // Factory for the proxy tool +} +``` + +The tool name is sanitized to contain only letters, digits, and underscores — ensuring compatibility with AI model function-calling requirements. + +## Agent Proxy Execution + +When the AI model decides to invoke a remote agent skill, `A2AAgentProxyTool` handles the execution. + +### Input Schema + +```json +{ + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The message or task to send to the remote agent for processing." + }, + "contextId": { + "type": "string", + "description": "An optional context identifier to maintain conversation continuity with the remote agent." + } + }, + "required": ["message"] +} +``` + +### Execution Flow + +```text +1. Validate input — "message" is required +2. Create HttpClient via IHttpClientFactory +3. Load connection from ICatalog +4. Read A2AConnectionMetadata from connection properties +5. Configure HttpClient with authentication headers +6. Create A2AClient pointing at the remote endpoint +7. Build AgentMessage: + - Role: User + - MessageId: new GUID + - ContextId: provided or new GUID + - Parts: [TextPart with the message] + - Metadata: { "agentName": skill name } +8. Send message via client.SendMessageAsync() +9. Extract text from response: + - AgentMessage → join TextParts + - AgentTask → check Artifacts, then Status.Message +10. Return text to the AI model +``` + +### Response Handling + +The proxy tool handles two types of A2A responses: + +- **`AgentMessage`** — Direct response with text parts (synchronous completion) +- **`AgentTask`** — Task-based response where text may be in artifacts or status messages (async workflows) + +If no text can be extracted, the tool returns: `"The remote agent did not produce a text response."` + +If communication fails, the error is logged and a user-friendly message is returned (the exception is not propagated to the AI model). + +## Built-in Discovery Tools + +The A2A client registers three system tools that help the AI model discover available agents and tools at runtime. + +### `ListAvailableAgentsFunction` + +Lists **all** available agents — both local AI Agent profiles and remote agents from A2A connections. + +- **Tool name**: `listAvailableAgents` +- **Parameters**: None +- **Returns**: JSON array of agents with `name`, `id`, `description`, `source` ("local" or "remote"), and optionally `host` and `tags` + +```text +AI Model: "What agents are available?" +→ Calls listAvailableAgents +→ Returns: +[ + { "name": "Code Reviewer", "id": "code-reviewer", "source": "local" }, + { "name": "Legal Analyst", "id": "legal-analyst", "source": "remote", "host": "Legal Team" } +] +``` + +### `FindAgentForTaskFunction` + +Finds the most relevant agents for a given task using keyword and semantic matching. + +- **Tool name**: `findAgentForTask` +- **Parameters**: + - `taskDescription` (string, required) — What the task is about + - `maxResults` (integer, optional) — Maximum agents to return (default: 5) +- **Returns**: JSON array of agents ranked by relevance score + +The function tokenizes the task description and scores each agent based on keyword overlap between the query and the agent's name + description + tags. Both forward and reverse matching are used to balance precision and recall. + +### `FindToolsForTaskFunction` + +Discovers tools (including remote agent skills) relevant to a given task. + +- **Tool name**: `findToolsForTask` +- **Parameters**: + - `taskDescription` (string, required) — What the task is about + - `maxResults` (integer, optional) — Maximum tools to return (default: 10) +- **Returns**: JSON array of tools with `name`, `description`, and `source` + +This function delegates to `IToolRegistry.SearchAsync()` to use the same scoring logic as the orchestrator's tool-scoping system. It automatically includes all A2A connections when building the search context. + +## Authentication + +### `IA2AConnectionAuthService` + +The authentication service builds HTTP headers for each connection based on its configured authentication type: + +```csharp +public interface IA2AConnectionAuthService +{ + /// Builds authentication headers from connection metadata. + Task> BuildHeadersAsync( + A2AConnectionMetadata metadata, + CancellationToken cancellationToken = default); + + /// Configures an HttpClient with authentication headers. + Task ConfigureHttpClientAsync( + HttpClient httpClient, + A2AConnectionMetadata metadata, + CancellationToken cancellationToken = default); +} +``` + +### Supported Authentication Types + +Authentication metadata is stored in `A2AConnectionMetadata`: + +| Type | Enum Value | How It Works | +|------|-----------|--------------| +| **Anonymous** | `Anonymous` | No authentication headers added | +| **API Key** | `ApiKey` | Sends the key in a configurable header (default: `Authorization`) with optional prefix (e.g., `Bearer`) | +| **Basic** | `Basic` | Base64-encodes `username:password` and sends as `Authorization: Basic {encoded}` | +| **OAuth2 Client Credentials** | `OAuth2ClientCredentials` | Exchanges `client_id` + `client_secret` for a bearer token at the token endpoint | +| **OAuth2 Private Key JWT** | `OAuth2PrivateKeyJwt` | Creates a signed JWT assertion using an RSA private key and exchanges it for a token | +| **OAuth2 Mutual TLS** | `OAuth2Mtls` | Uses a client certificate for mutual TLS authentication when requesting a token | +| **Custom Headers** | `CustomHeaders` | Sends arbitrary key-value pairs as HTTP headers | + +### `A2AConnectionMetadata` Properties + +```csharp +public sealed class A2AConnectionMetadata +{ + // Which authentication type to use + public A2AClientAuthenticationType AuthenticationType { get; set; } + + // API Key authentication + public string ApiKeyHeaderName { get; set; } // Default: "Authorization" + public string ApiKeyPrefix { get; set; } // e.g., "Bearer" + public string ApiKey { get; set; } // The key (encrypted via DataProtection) + + // Basic authentication + public string BasicUsername { get; set; } + public string BasicPassword { get; set; } // Encrypted via DataProtection + + // OAuth 2.0 Client Credentials + public string OAuth2TokenEndpoint { get; set; } + public string OAuth2ClientId { get; set; } + public string OAuth2ClientSecret { get; set; } // Encrypted via DataProtection + public string OAuth2Scopes { get; set; } + + // OAuth 2.0 Private Key JWT + public string OAuth2PrivateKey { get; set; } // PEM-encoded RSA private key (encrypted) + public string OAuth2KeyId { get; set; } + + // OAuth 2.0 Mutual TLS (mTLS) + public string OAuth2ClientCertificate { get; set; } // Base64 PKCS#12 (encrypted) + public string OAuth2ClientCertificatePassword { get; set; } // Encrypted + + // Custom headers + public Dictionary AdditionalHeaders { get; set; } +} +``` + +### Credential Protection + +All sensitive fields (API keys, passwords, secrets, private keys, certificates) are encrypted using ASP.NET Core Data Protection with the purpose string `"A2AClientConnection"`. The `DefaultA2AConnectionAuthService` automatically decrypts values before use. + +OAuth2 tokens are cached in `IMemoryCache` with a TTL based on the token's `expires_in` minus a 60-second buffer. + +### Custom Authentication + +To implement a custom authentication scheme, register your own `IA2AConnectionAuthService`: + +```csharp +public sealed class MyA2AAuthService : IA2AConnectionAuthService +{ + public Task> BuildHeadersAsync( + A2AConnectionMetadata metadata, + CancellationToken cancellationToken = default) + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Your custom logic — e.g., fetch tokens from a vault + headers["Authorization"] = "Bearer " + GetTokenFromVault(); + + return Task.FromResult(headers); + } + + public async Task ConfigureHttpClientAsync( + HttpClient httpClient, + A2AConnectionMetadata metadata, + CancellationToken cancellationToken = default) + { + var headers = await BuildHeadersAsync(metadata, cancellationToken); + + foreach (var header in headers) + { + httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); + } + } +} + +// Register (replaces the default) +builder.Services.AddScoped(); +``` + +## Configuration Examples + +### Minimal Setup (anonymous remote agent) + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration() + .AddCoreAIA2AClient(); + +// Register your connection store +builder.Services.AddScoped, MyA2AConnectionStore>(); +``` + +### With API Key Authentication + +```csharp +// When creating a connection, store the metadata: +var connection = new A2AConnection +{ + DisplayText = "Partner Translation Service", + Endpoint = "https://translate.partner.com/a2a", +}; + +var metadata = new A2AConnectionMetadata +{ + AuthenticationType = A2AClientAuthenticationType.ApiKey, + ApiKeyHeaderName = "Authorization", + ApiKeyPrefix = "Bearer", + ApiKey = protector.Protect("sk-partner-key-12345"), +}; + +connection.Put(metadata); + +await connectionStore.CreateAsync(connection); +``` + +### With OAuth2 Client Credentials + +```csharp +var metadata = new A2AConnectionMetadata +{ + AuthenticationType = A2AClientAuthenticationType.OAuth2ClientCredentials, + OAuth2TokenEndpoint = "https://auth.partner.com/oauth2/token", + OAuth2ClientId = "my-app-client-id", + OAuth2ClientSecret = protector.Protect("my-client-secret"), + OAuth2Scopes = "a2a.invoke", +}; +``` + + + +- Admin UI for creating and managing A2A connections +- Built-in `ICatalog` backed by YesSql +- Authentication configuration forms for all supported types +- Connection assignment to AI profiles via the profile editor +- Agent card preview and cache invalidation diff --git a/src/CrestApps.Core.Docs/docs/a2a/host.md b/src/CrestApps.Core.Docs/docs/a2a/host.md new file mode 100644 index 00000000..77d43284 --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/a2a/host.md @@ -0,0 +1,321 @@ +--- +sidebar_label: A2A Host +sidebar_position: 3 +title: A2A Host +description: Expose your AI agents to remote clients using the A2A protocol host — configuration, authentication modes, agent card generation, and skill exposure. +--- + +# A2A Host + +> Expose your AI agents to remote A2A clients so they can discover and invoke your agents over HTTP. + +## Quick Start + +```csharp +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.ApiKey; + options.ApiKey = "your-secret-api-key"; +}); +``` + +## Problem & Solution + +You have AI agents (profiles) running in your application and you want other applications to be able to discover and invoke them. The A2A host configuration: + +- **Publishes** your agents via Agent Cards at a well-known endpoint +- **Authenticates** incoming requests using OpenID Connect, API keys, or no auth +- **Authorizes** access with optional permission checks +- **Controls** whether agents appear as individual agent cards or as skills of a single combined card + + +## Host Configuration + +### `A2AHostOptions` + +All host behavior is controlled through `A2AHostOptions`: + +```csharp +public sealed class A2AHostOptions +{ + /// The authentication type for incoming A2A requests. + /// Default: OpenId + public A2AHostAuthenticationType AuthenticationType { get; set; } + = A2AHostAuthenticationType.OpenId; + + /// The API key required when AuthenticationType is ApiKey. + public string ApiKey { get; set; } + + /// Whether to require the AccessA2AHost permission. + /// Only applies to OpenId authentication. Default: true + public bool RequireAccessPermission { get; set; } = true; + + /// Whether to expose all agents as skills of a single combined agent card. + /// When false (default), each agent gets its own agent card. + public bool ExposeAgentsAsSkill { get; set; } = false; +} +``` + +### Configuration via `IServiceCollection` + +```csharp +// In Program.cs or Startup.cs +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.OpenId; + options.RequireAccessPermission = true; +}); +``` + +### Configuration via `appsettings.json` + +```json +{ + "A2AHost": { + "AuthenticationType": "ApiKey", + "ApiKey": "your-secret-api-key", + "RequireAccessPermission": true, + "ExposeAgentsAsSkill": false + } +} +``` + +```csharp +builder.Services.Configure( + builder.Configuration.GetSection("A2AHost")); +``` + +## Authentication Modes + +The host supports three authentication types via `A2AHostAuthenticationType`: + +### OpenID Connect (`OpenId`) — Default + +The most secure option for production. Incoming requests are authenticated using the `"Api"` OpenID Connect scheme. Tokens are validated against your OpenID provider. + +```csharp +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.OpenId; + options.RequireAccessPermission = true; // Require AccessA2AHost permission +}); +``` + +When `RequireAccessPermission` is `true`, the authenticated user must also have the `AccessA2AHost` permission. When `false`, any valid authenticated user can access the host. + +:::tip +::: + +### API Key (`ApiKey`) + +A simple shared-secret authentication. The client must send the API key in the `Authorization` header: + +```text +Authorization: Bearer your-secret-api-key +``` + +```csharp +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.ApiKey; + options.ApiKey = "your-secret-api-key"; +}); +``` + +:::warning +Store the API key in a secure location (environment variable, Azure Key Vault, etc.). Never hardcode it in source code. The `RequireAccessPermission` option does **not** apply to API key authentication. +::: + +### None — Development Only + +Disables all authentication. Any request is accepted. + +```csharp +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.None; +}); +``` + +:::danger +**Never use `None` in production.** This option exists solely for local development and testing. +::: + +### Authentication Comparison + +| Feature | OpenId | ApiKey | None | +|---------|--------|--------|------| +| **Security level** | High | Medium | ❌ None | +| **Token validation** | ✅ JWT/OIDC | ❌ Shared secret | ❌ | +| **User identity** | ✅ Full claims | ❌ Anonymous | ❌ Anonymous | +| **Permission checks** | ✅ Optional | ❌ | ❌ | +| **Best for** | Production | Internal/partner APIs | Local dev | + +## Agent Card Generation + +### How Profiles Become Agent Cards + +When a remote client fetches the Agent Card from your host, the host implementation reads your AI profiles and converts them into the A2A Agent Card format: + +```text +┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ +│ AI Profile │────────►│ Agent Card │────────►│ /.well-known/ │ +│ (your app) │ │ Generator │ │ agent.json │ +│ │ │ │ │ │ +│ Name │ │ Name │ │ Published to remote │ +│ Description │ │ Description │ │ A2A clients │ +│ Type: Agent │ │ Skills [] │ │ │ +└──────────────┘ └──────────────┘ └──────────────────────┘ +``` + +Each AI profile of type `Agent` becomes either: +- An **independent Agent Card** (default behavior), or +- A **skill on a combined Agent Card** (when `ExposeAgentsAsSkill` is `true`) + +### Agent Card Structure (A2A Protocol) + +A published Agent Card follows the A2A specification: + +```json +{ + "name": "My AI Assistant", + "description": "An AI assistant that can help with various tasks.", + "url": "https://myapp.example.com/a2a", + "skills": [ + { + "id": "translate-text", + "name": "Text Translator", + "description": "Translates text between languages.", + "tags": ["translation", "language"] + }, + { + "id": "summarize-document", + "name": "Document Summarizer", + "description": "Summarizes long documents into key points.", + "tags": ["summarization", "documents"] + } + ] +} +``` + +## Skill Exposure + +### Individual Agent Cards (Default) + +When `ExposeAgentsAsSkill` is `false` (the default), each agent profile is exposed as its own independent Agent Card. Remote clients see separate agents and can invoke them individually. + +```text +Profile: "Code Reviewer" → Agent Card: { name: "Code Reviewer", skills: [...] } +Profile: "Translator" → Agent Card: { name: "Translator", skills: [...] } +``` + +This is the recommended approach when your agents are independent and serve different purposes. + +### Combined Agent Card + +When `ExposeAgentsAsSkill` is `true`, a single Agent Card is published with each agent profile listed as a skill: + +```text +Combined Agent Card: { + name: "My Application", + skills: [ + { id: "code-reviewer", name: "Code Reviewer", ... }, + { id: "translator", name: "Translator", ... } + ] +} +``` + +This approach is useful when: +- You want remote clients to see a **single entry point** to your application +- The client's AI model should choose which skill to invoke based on the task +- You want to simplify discovery for clients that don't need to manage multiple connections + +```csharp +builder.Services.Configure(options => +{ + options.ExposeAgentsAsSkill = true; +}); +``` + +## Endpoint Setup + +The A2A protocol defines two key endpoints that your host must serve: + +### Agent Card Endpoint + +Remote clients discover your agents by fetching the Agent Card: + +```text +GET /.well-known/agent.json +``` + +This returns the Agent Card JSON (or multiple cards, depending on your `ExposeAgentsAsSkill` setting). + +### Message Endpoint + +Remote clients send tasks to your agents via: + +```text +POST /a2a +Content-Type: application/json + +{ + "message": { + "role": "user", + "messageId": "msg-123", + "contextId": "ctx-456", + "parts": [{ "type": "text", "text": "Translate this to French: Hello world" }] + } +} +``` + +The host routes the message to the appropriate AI profile, processes it, and returns a response. + +### Implementation Pattern + +The actual endpoint implementation depends on your application framework. Here is a conceptual pattern: + +```csharp +// Agent Card endpoint +app.MapGet("/.well-known/agent.json", async ( + IOptions options, + IAIProfileManager profileManager) => +{ + // 1. Load agent profiles + // 2. Convert to Agent Card format + // 3. Return JSON +}); + +// Message endpoint +app.MapPost("/a2a", async ( + HttpContext context, + IOptions options, + IAICompletionService completionService) => +{ + // 1. Authenticate the request based on A2AHostOptions + // 2. Parse the incoming AgentMessage + // 3. Route to the appropriate AI profile + // 4. Process and return the response +}); +``` + +:::info +::: + +## Security Best Practices + +1. **Always use OpenID or API Key authentication in production** — never deploy with `AuthenticationType = None` +2. **Rotate API keys regularly** — treat them as secrets with a defined rotation policy +3. **Use `RequireAccessPermission = true`** with OpenID — this ensures only authorized users/applications can invoke your agents +4. **Restrict agent exposure** — only expose agent profiles that are intended for remote consumption +5. **Monitor agent invocations** — log and audit incoming A2A requests +6. **Use HTTPS** — the A2A protocol sends messages over HTTP; always use TLS in production + + + +- Full endpoint implementation (Agent Card + message endpoints) +- Admin UI for configuring `A2AHostOptions` +- Automatic conversion of AI profiles to Agent Cards +- Built-in authentication middleware for all three modes +- Permission management for the `AccessA2AHost` permission +- Support for both individual and combined Agent Card modes diff --git a/src/CrestApps.Core.Docs/docs/a2a/index.md b/src/CrestApps.Core.Docs/docs/a2a/index.md new file mode 100644 index 00000000..1bf945ad --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/a2a/index.md @@ -0,0 +1,107 @@ +--- +sidebar_label: Overview +sidebar_position: 1 +title: Agent-to-Agent Protocol (A2A) +description: Connect to remote AI agents and expose your own agents using the A2A protocol for cross-application agent collaboration. +--- + +# Agent-to-Agent Protocol (A2A) + +> Discover, invoke, and expose AI agents across application boundaries using the [Agent-to-Agent (A2A) protocol](https://google.github.io/A2A/). + +## What Is A2A? + +The Agent-to-Agent (A2A) protocol, developed by Google, is an open standard that enables AI agents running in **different applications** to discover each other, negotiate capabilities, and delegate tasks — all over HTTP. Unlike tool-calling protocols that expose individual functions, A2A operates at the **agent level**: a remote agent is a self-contained entity with its own reasoning, tools, and context. + +Key concepts: + +| Concept | Description | +|---------|-------------| +| **Agent Card** | A JSON document published by a host that describes the agent's name, description, skills, and endpoint URL. Clients fetch this to discover what a host offers. | +| **Skill** | A named capability advertised on an Agent Card (e.g., "translate-text", "summarize-document"). Each skill becomes an invokable tool on the client side. | +| **Host** | An application that **exposes** one or more AI agents to remote clients. | +| **Client** | An application that **discovers and invokes** remote agents hosted elsewhere. | +| **Message** | The unit of communication — a client sends an `AgentMessage` to the host and receives a response containing text, artifacts, or task status. | + +## When to Use A2A vs MCP + +Both protocols connect AI systems across boundaries, but they solve different problems: + +| Criteria | A2A | MCP | +|----------|-----|-----| +| **Abstraction level** | Agent-level (send a task, get a result) | Tool-level (call a function, get a return value) | +| **Best for** | Delegating complex, multi-step work to a remote AI agent | Exposing individual functions, data sources, or resources | +| **Remote agent has its own AI model?** | ✅ Yes — the remote agent reasons independently | ❌ No — tools are stateless functions | +| **Conversation context** | Maintained via `contextId` across messages | Stateless per tool call | +| **Discovery** | Agent Cards with skills | Tool lists with JSON schemas | +| **Use when** | "Ask the legal team's agent to review this contract" | "Call the weather API to get today's forecast" | + +**Rule of thumb**: If the remote system needs to **think** (use an AI model, maintain context, choose its own tools), use A2A. If it just needs to **do** (execute a function and return data), use MCP. + +You can use both in the same application — A2A for agent delegation and MCP for tool access. + +## Architecture + +```text +┌─────────────────────────────────┐ ┌──────────────────────────────────┐ +│ A2A CLIENT │ │ A2A HOST │ +│ │ │ │ +│ ┌───────────┐ │ HTTP │ ┌──────────────┐ │ +│ │ AI Model │ │ ◄──────► │ │ AI Profiles │ │ +│ └─────┬─────┘ │ │ └──────┬───────┘ │ +│ │ tool call │ │ │ │ +│ ┌─────▼──────────────────┐ │ │ ┌───────────────────▼────────┐ │ +│ │ A2AToolRegistryProvider│ │ │ │ Agent Card Generator │ │ +│ │ (discovers skills as │ │ │ │ (profiles → agent cards) │ │ +│ │ tool entries) │ │ │ └───────────────────┬────────┘ │ +│ └─────┬──────────────────┘ │ │ │ │ +│ │ │ │ ┌──────────────────▼─────────┐ │ +│ ┌─────▼──────────────────┐ │ fetch │ │ /.well-known/agent.json │ │ +│ │ A2AAgentProxyTool ├─────┼──────────┼──► (Agent Card endpoint) │ │ +│ │ (proxies messages │ │ │ └───────────────────────────┘ │ +│ │ to remote agent) │ │ send │ │ +│ │ ├─────┼──────────┼──► /a2a (message endpoint) │ +│ └────────────────────────┘ │ │ │ +│ │ │ Authentication: │ +│ Authentication: │ │ • OpenID Connect │ +│ • API Key, Basic, OAuth2, │ │ • API Key │ +│ mTLS, Custom Headers │ │ • None (dev only) │ +└─────────────────────────────────┘ └──────────────────────────────────┘ +``` + +## Quick Start + +### As a Client (invoke remote agents) + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration() + .AddCoreAIA2AClient(); +``` + +→ See the [A2A Client](./client) page for connection setup, authentication, and tool registry details. + +### As a Host (expose your agents) + +```csharp +// Host configuration is done via A2AHostOptions +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.ApiKey; + options.ApiKey = "your-secret-key"; +}); +``` + +→ See the [A2A Host](./host) page for authentication modes, agent card generation, and endpoint configuration. + +## Sub-Pages + +| Page | Description | +|------|-------------| +| [A2A Client](./client) | Discover and invoke remote A2A agents — connection management, tool registry, authentication, built-in discovery tools | +| [A2A Host](./host) | Expose your AI agents to remote clients — host configuration, authentication modes, agent card generation | + + +The framework-level A2A support documented here is protocol infrastructure. For the full admin UI experience: + diff --git a/src/CrestApps.Core.Docs/docs/changelog/index.md b/src/CrestApps.Core.Docs/docs/changelog/index.md new file mode 100644 index 00000000..9dde1345 --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/changelog/index.md @@ -0,0 +1,14 @@ +--- +sidebar_label: Overview +sidebar_position: 0 +title: Changelog +description: Release history and migration notes for CrestApps.Core. +--- + +# Changelog + +This section tracks `CrestApps.Core` releases and notable repository-level changes. + +| Version | Highlights | +| --- | --- | +| [1.0.0](v1.0.0) | Initial standalone release of the `CrestApps.Core` framework repository | diff --git a/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md new file mode 100644 index 00000000..c50adbbd --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md @@ -0,0 +1,19 @@ +--- +sidebar_label: 1.0.0 Release Notes +sidebar_position: 2 +title: "Version 1.0.0 Release Notes" +description: Initial standalone release notes for the CrestApps.Core repository. +--- + +# Version 1.0.0 Release Notes + +**Package version**: `1.0.0` + +`CrestApps.Core` 1.0.0 establishes the standalone framework repository for the reusable CrestApps libraries. + +## Highlights + +- ships the shared abstractions, infrastructure, AI runtime, provider integrations, and protocol packages under the `CrestApps.Core` name +- includes a reference MVC host and an Aspire host for local composition and testing +- includes a dedicated `CrestApps.Core.Tests` project for framework validation +- publishes a framework-focused documentation site at [core.crestapps.com](https://core.crestapps.com) diff --git a/src/CrestApps.Core.Docs/docs/core/agents.md b/src/CrestApps.Core.Docs/docs/core/agents.md new file mode 100644 index 00000000..8042c3ba --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/core/agents.md @@ -0,0 +1,263 @@ +--- +sidebar_label: AI Agents +sidebar_position: 15 +title: AI Agents +description: Delegate tasks to specialized sub-agents that the primary AI model can invoke as tools during orchestration. +--- + +# AI Agents + +> Purpose-built AI profiles that the primary model can invoke as tools — each with its own system prompt, deployment, and capabilities. + +## Quick Start + +Agents are available automatically when orchestration is enabled: + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration(); // registers AgentToolRegistryProvider +``` + +Create an agent profile, then link it to a chat profile: + +```csharp +// 1. Create an agent profile +var agent = new AIProfile +{ + Type = AIProfileType.Agent, + Name = "code-reviewer", + DisplayText = "Code Reviewer", + Description = "Reviews code for bugs, security issues, and best practices.", + ChatDeploymentName = "gpt-4o-deployment", +}; +agent.Put(new AgentMetadata { Availability = AgentAvailability.OnDemand }); + +await profileManager.CreateAsync(agent); + +// 2. Link it to a chat profile +chatProfile.Put(new AgentInvocationMetadata { Names = ["code-reviewer"] }); +await profileManager.UpdateAsync(chatProfile); +``` + +The primary model can now call the `code-reviewer` agent as a tool during orchestration. + +## Problem & Solution + +A single AI profile often needs to handle diverse tasks — code review, translation, data analysis, summarization. Cramming all instructions into one system prompt leads to: + +- **Conflicting instructions** — a translator prompt fights with a code review prompt +- **Model confusion** — the model struggles with broad, unfocused responsibilities +- **No isolation** — all tasks share the same deployment, token limits, and context + +Agents solve this by allowing the primary model to **delegate** to specialized sub-agents: + +| Concern | Without Agents | With Agents | +|---------|---------------|-------------| +| System prompt | One monolithic prompt for all tasks | Each agent has a focused prompt | +| Model selection | Single deployment for everything | Each agent can use a different deployment | +| Token budget | Shared across all capabilities | Each agent runs its own completion | +| Scope | Everything in one context | Isolated per-task context | + +## How Agents Work + +``` +User message + │ + ▼ +┌──────────────────┐ +│ Primary Model │ ← Chat profile with tools + agents +│ (Orchestrator) │ +└────────┬─────────┘ + │ calls agent tool + ▼ +┌──────────────────┐ +│ AgentProxyTool │ ← Receives { "prompt": "Review this code..." } +└────────┬─────────┘ + │ builds agent context + ▼ +┌──────────────────┐ +│ Agent Model │ ← Agent profile (own system prompt, deployment) +│ (tools disabled) │ +└────────┬─────────┘ + │ returns response + ▼ +┌──────────────────┐ +│ Primary Model │ ← Incorporates agent's response and continues +│ (continues) │ +└──────────────────┘ +``` + +The primary model sees each agent as a regular tool with a `prompt` parameter. It decides when and how to invoke agents based on the user's request and the agent descriptions injected into the system message. + +## Agent Availability + +The `AgentAvailability` enum controls when an agent is included in orchestration: + +| Mode | Behavior | Use Case | +|------|----------|----------| +| `OnDemand` | Included only when explicitly listed in `AgentInvocationMetadata` on the chat profile | Specialized agents (code review, translation) assigned per profile | +| `AlwaysAvailable` | Automatically included in every orchestration request | Core agents needed globally (safety checker, logging agent) | + +```csharp +// On-demand: only available when a chat profile explicitly requests it +agent.Put(new AgentMetadata { Availability = AgentAvailability.OnDemand }); + +// Always available: included in every request automatically +agent.Put(new AgentMetadata { Availability = AgentAvailability.AlwaysAvailable }); +``` + +**Token considerations:** `AlwaysAvailable` agents increase token usage on every request because their descriptions are always present in the system message and their tool definitions are always registered. Use `OnDemand` to minimize cost. + +## Creating Agent Profiles + +Agent profiles are standard `AIProfile` objects with `Type = AIProfileType.Agent`. They require a `Name` and `Description` at minimum — the description is what the primary model sees when deciding whether to invoke the agent. + +```csharp +var translatorAgent = new AIProfile +{ + Type = AIProfileType.Agent, + Name = "translator", + DisplayText = "Translator", + Description = "Translates text between languages. Provide the target language and text to translate.", + ChatDeploymentName = "gpt-4o-mini-deployment", +}; +translatorAgent.Put(new AgentMetadata +{ + Availability = AgentAvailability.OnDemand, +}); + +await profileManager.CreateAsync(translatorAgent); +``` + +### Required Fields + +| Field | Purpose | +|-------|---------| +| `Type` | Must be `AIProfileType.Agent` | +| `Name` | Unique identifier used as the tool name (becomes `agent:{name}` in the registry) | +| `Description` | Shown to the primary model — drives its decision to invoke this agent | +| `ChatDeploymentName` | The AI deployment used for the agent's completion | + +### Optional Configuration + +- **System message** — Configure via templates or the profile's system message property +- **AgentMetadata** — Set availability mode (`OnDemand` or `AlwaysAvailable`) + +Agents with an empty `Name` or `Description` are silently skipped during registration. + +## Linking Agents to Chat Profiles + +On-demand agents must be explicitly linked to a chat profile via `AgentInvocationMetadata`: + +```csharp +// Make specific agents available to this chat profile +chatProfile.Put(new AgentInvocationMetadata +{ + Names = ["code-reviewer", "translator", "summarizer"], +}); + +await profileManager.UpdateAsync(chatProfile); +``` + +The `Names` array maps to agent profile names. At orchestration time, the `AgentToolRegistryProvider` reads these names from `AICompletionContext.AgentNames` and includes only matching agents. + +`AlwaysAvailable` agents do **not** need to be listed here — they are included automatically regardless of `AgentInvocationMetadata`. + +## Agent Execution Flow + +When the primary model invokes an agent tool, the following sequence occurs inside `AgentProxyTool`: + +1. **Parse input** — Extract the `prompt` string from the tool call arguments +2. **Resolve agent profile** — Look up the agent by name via `IAIProfileManager.GetAsync(AIProfileType.Agent)` +3. **Build agent context** — Call `IAICompletionContextBuilder.BuildAsync(agentProfile)` to construct the agent's own completion context (system message, settings, etc.) +4. **Disable tools** — Set `context.DisableTools = true` on the agent's context (see [Recursion Prevention](#recursion-prevention)) +5. **Resolve deployment** — Find the chat deployment via `IAIDeploymentManager.ResolveOrDefaultAsync()` +6. **Send prompt** — Create a single `ChatMessage` with `ChatRole.User` containing the prompt +7. **Execute completion** — Call `IAICompletionService.CompleteAsync()` with the agent's deployment, messages, and context +8. **Return response** — Extract the assistant's response text and return it to the primary model + +```csharp +// Simplified flow inside AgentProxyTool.InvokeCoreAsync: +var context = await contextBuilder.BuildAsync(agentProfile); +context.DisableTools = true; + +var deployment = await deploymentManager.ResolveOrDefaultAsync( + AIDeploymentType.Chat, deploymentName: context.ChatDeploymentName); + +var messages = new List +{ + new(ChatRole.User, task), +}; + +var response = await completionService.CompleteAsync( + deployment, messages, context, cancellationToken); +``` + +If the agent profile is not found or an error occurs, `AgentProxyTool` returns a descriptive error message to the primary model rather than throwing — allowing the orchestration to continue gracefully. + +## Recursion Prevention + +Without safeguards, an agent could invoke other agents (or itself), creating an infinite loop. The framework prevents this by **disabling tools on the agent's completion context**: + +```csharp +context.DisableTools = true; +``` + +This means: + +- Agents **cannot** call tools, including other agents +- Agents run a single, isolated completion with their own system prompt and the provided prompt +- The agent's response is pure text — no tool calls, no further delegation + +This is a deliberate design choice that keeps agent execution predictable and bounded. If you need multi-level delegation, compose it at the chat profile level by having multiple agents available to the primary model, which can invoke them sequentially. + +## System Message Enrichment + +The `AgentOrchestrationContextBuilderHandler` automatically enriches the primary model's system message with descriptions of all available agents. This gives the model awareness of which agents exist and what they can do, enabling informed routing decisions. + +The handler: + +1. Reads all agent profiles via `IAIProfileManager` +2. Filters to agents matching the availability criteria +3. Renders agent descriptions using the `AITemplateIds.AgentAvailability` template +4. Appends the rendered text to the orchestration context's `SystemMessageBuilder` + +This follows the industry-standard pattern used by orchestration frameworks where agent descriptions are included in the system prompt so the model can decide which capabilities to invoke. + +## Implementing `IAIProfileManager` + +The host application must provide an implementation of `IAIProfileManager` for agents to work. The agent subsystem relies on two key operations: + +```csharp +public interface IAIProfileManager +{ + // Used by AgentToolRegistryProvider and AgentProxyTool to fetch agent profiles + Task> GetAsync(AIProfileType type); + + // Used to persist agent profiles + Task CreateAsync(AIProfile profile); + Task UpdateAsync(AIProfile profile); +} +``` + +The `GetAsync(AIProfileType.Agent)` call is the primary query used by: + +- **`AgentToolRegistryProvider`** — to discover agents and build tool entries +- **`AgentProxyTool`** — to resolve the target agent at invocation time +- **`AgentOrchestrationContextBuilderHandler`** — to enrich the system message with agent descriptions + +Your implementation must return agent profiles with their `Properties` intact (including `AgentMetadata`) for availability filtering to work correctly. + +## Services Registered + +`AddCoreAIOrchestration()` registers the following agent-related services: + +| Service | Implementation | Purpose | +|---------|---------------|---------| +| `IToolRegistryProvider` | `AgentToolRegistryProvider` | Exposes agents as tool entries | +| `IOrchestrationContextBuilderHandler` | `AgentOrchestrationContextBuilderHandler` | Enriches system message with agent descriptions | + +Both are registered as **scoped** services via `TryAddEnumerable`, ensuring they participate alongside other tool providers and context handlers. + + diff --git a/src/CrestApps.Core.Docs/docs/core/ai-core.md b/src/CrestApps.Core.Docs/docs/core/ai-core.md new file mode 100644 index 00000000..bb57c1a4 --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/core/ai-core.md @@ -0,0 +1,436 @@ +--- +sidebar_label: AI Core +sidebar_position: 3 +title: AI Core +description: Core AI services including completion clients, client factory, context building, and the deployment resolution chain. +--- + +# AI Core + +> Provider-agnostic AI completion services, client factory, and context-building pipeline. + +## Quick Start + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOpenAI(); // or any other provider +``` + +This gives you access to `IAIClientFactory`, `IAICompletionService`, and `IAICompletionContextBuilder`. + +## Problem & Solution + +AI applications need to work with multiple LLM providers (OpenAI, Azure, Ollama, etc.) without coupling business logic to a specific SDK. The AI Core layer provides a **provider-agnostic abstraction** where you program against interfaces and swap providers through configuration. + +## Core Concepts + +### Deployment + +A **deployment** maps a logical name to a specific model on a specific provider connection. For example, deployment `"gpt-4o"` might map to the `gpt-4o` model on your OpenAI connection. The orchestrator resolves deployments at runtime using a fallback chain: + +1. Profile-level deployment override +2. Connection-level default deployment +3. Global default deployment + +### Provider Connection + +A **provider connection** stores credentials and endpoint information for a specific AI provider (API key, endpoint URL, provider name). + +## Services Registered by `AddCoreAIServices()` + +| Service | Implementation | Lifetime | Purpose | +|---------|---------------|----------|---------| +| `IAIClientFactory` | `DefaultAIClientFactory` | Scoped | Creates typed AI clients | +| `IAICompletionService` | `DefaultAICompletionService` | Scoped | Deployment-aware completion | +| `IAICompletionContextBuilder` | `DefaultAICompletionContextBuilder` | Scoped | Builds context with handler pipeline | +| `ITemplateService` | *(from AddCoreAITemplating)* | Scoped | Template rendering | + +It also chains `AddCoreAITemplating()` and `AddCoreServices()` automatically. + +Optional format-specific packages stay opt-in. For example, Markdown-aware normalization lives in `CrestApps.Core.AI.Markdown`, so hosts that want Markdig-backed RAG normalization should register `AddCoreAIMarkdown()` explicitly instead of expecting `AddCoreAIServices()` to pull it in automatically. + +## Key Interfaces + +### `IAIClientFactory` + +The lowest-level service. Creates typed AI clients from a provider connection entry. + +```csharp +public interface IAIClientFactory +{ + IChatClient CreateChatClient(AIProviderConnectionEntry connection, string deploymentName); + IEmbeddingGenerator> CreateEmbeddingGenerator( + AIProviderConnectionEntry connection, string deploymentName); + // Also: CreateImageGenerator, CreateSpeechToTextClient, CreateTextToSpeechClient +} +``` + +**When to use:** Only when you need direct, low-level access to a specific client type. + +### `IAICompletionService` + +Mid-level service that resolves a deployment and sends a completion request. + +```csharp +public interface IAICompletionService +{ + Task CompleteAsync( + AIDeployment deployment, + IList messages, + ChatOptions options = null, + CancellationToken cancellationToken = default); + + IAsyncEnumerable CompleteStreamingAsync( + AIDeployment deployment, + IList messages, + ChatOptions options = null, + CancellationToken cancellationToken = default); +} +``` + +**When to use:** When you have a deployment reference and want completion without the full orchestration loop. + +### `IAICompletionContextBuilder` + +Builds an `AICompletionContext` by running a handler pipeline that enriches the context before and after construction. + +```csharp +public interface IAICompletionContextBuilder +{ + ValueTask BuildAsync( + AICompletionContextBuildingContext context, + CancellationToken cancellationToken = default); +} +``` + +The builder invokes all registered `IAICompletionContextBuilderHandler` instances in sequence. See [Context Builders](./context-builders.md) for details. + +### `IAICompletionClient` + +Implement this interface to add a new AI provider. Each provider registers its own completion client. + +```csharp +public interface IAICompletionClient +{ + Task CompleteAsync( + AICompletionContext context, + CancellationToken cancellationToken = default); + + IAsyncEnumerable CompleteStreamingAsync( + AICompletionContext context, + CancellationToken cancellationToken = default); +} +``` + +**When to implement:** When integrating an AI provider not already supported. See [Providers](../providers/index.md). + +## Configuration + +### `AIOptions` + +Central options class for registering profile sources, deployment providers, connection sources, and template sources. + +```csharp +services.Configure(options => +{ + options.AddProfileSource("MySource", configure => { /* ... */ }); + options.AddDeploymentProvider("MyProvider", configure => { /* ... */ }); + options.AddConnectionSource("MySource", configure => { /* ... */ }); +}); +``` + +### `DefaultAIDeploymentSettings` + +Global default deployment settings, typically loaded from configuration: + +```json +{ + "CrestApps": { + "AI": { + "DefaultDeploymentName": "gpt-4o", + "DefaultConnectionName": "my-openai" + } + } +} +``` + +## Streaming Example + +Use `CompleteStreamingAsync` to stream tokens as they are generated: + +```csharp +public sealed class StreamingService(IAICompletionService completionService) +{ + public async IAsyncEnumerable StreamAsync( + AIDeployment deployment, + string question, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var messages = new List + { + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, question), + }; + + await foreach (var update in completionService.CompleteStreamingAsync( + deployment, messages, cancellationToken: cancellationToken)) + { + if (!string.IsNullOrEmpty(update.Text)) + { + yield return update.Text; + } + } + } +} +``` + +### Using Streaming in an API Controller + +```csharp +[ApiController] +[Route("api/[controller]")] +public sealed class ChatApiController : ControllerBase +{ + private readonly IAICompletionService _completionService; + + public ChatApiController(IAICompletionService completionService) + { + _completionService = completionService; + } + + [HttpPost("stream")] + public async Task StreamResponse( + [FromBody] ChatRequest request, + CancellationToken cancellationToken) + { + Response.ContentType = "text/event-stream"; + + var messages = new List + { + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, request.Message), + }; + + await foreach (var update in _completionService.CompleteStreamingAsync( + request.Deployment, messages, cancellationToken: cancellationToken)) + { + if (!string.IsNullOrEmpty(update.Text)) + { + await Response.WriteAsync($"data: {update.Text}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + } + } +} +``` + +## Error Handling + +### Common Exceptions + +| Exception | When | How to Handle | +|-----------|------|--------------| +| `InvalidOperationException` | No deployment found, no provider connection configured | Check AI configuration — this is a setup error | +| `HttpRequestException` | Provider API unreachable (network error, DNS failure) | Retry with exponential backoff, check network connectivity | +| `OperationCanceledException` | Request was cancelled (user navigated away, timeout) | Normal flow — let it propagate | +| Provider-specific rate limit errors | Too many requests to the AI provider | Implement retry policies at the HTTP client level | +| Provider-specific auth errors | Invalid API key or expired credentials | Check provider connection configuration | + +### Handling Provider Failures + +```csharp +public sealed class ResilientCompletionService +{ + private readonly IAICompletionService _completionService; + private readonly ILogger _logger; + + public ResilientCompletionService( + IAICompletionService completionService, + ILogger logger) + { + _completionService = completionService; + _logger = logger; + } + + public async Task SafeCompleteAsync( + AIDeployment deployment, + IList messages, + CancellationToken cancellationToken = default) + { + try + { + var response = await _completionService.CompleteAsync( + deployment, messages, cancellationToken: cancellationToken); + + return response.Text; + } + catch (OperationCanceledException) + { + throw; // Always re-throw cancellation + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, "AI configuration error — check deployment settings."); + throw; // Configuration errors should not be silently swallowed + } + catch (Exception ex) + { + _logger.LogError(ex, "AI completion failed for deployment '{Deployment}'.", + deployment.Name); + return null; // Or return a fallback message + } + } +} +``` + +:::warning +Never swallow `OperationCanceledException` — always re-throw it. Catching and ignoring it breaks the cancellation token contract and can cause resource leaks. +::: + +## Implementing a Custom AI Provider + +To integrate an AI provider that is not already supported (e.g., Anthropic, Mistral, Cohere), implement `IAICompletionClient`: + +```csharp +public interface IAICompletionClient +{ + string Name { get; } + + Task CompleteAsync( + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default); + + IAsyncEnumerable CompleteStreamingAsync( + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default); +} +``` + +### Example: Custom Provider Implementation + +```csharp +public sealed class MyProviderCompletionClient : IAICompletionClient +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public MyProviderCompletionClient( + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + public string Name => "MyProvider"; + + public async Task CompleteAsync( + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default) + { + var client = _httpClientFactory.CreateClient("MyProvider"); + + // Convert messages to your provider's API format + var request = new + { + model = context.Deployment.ModelName, + messages = messages.Select(m => new + { + role = m.Role.Value, + content = m.Text, + }), + max_tokens = context.Options?.MaxOutputTokens ?? 1024, + temperature = context.Options?.Temperature ?? 0.7f, + }; + + var response = await client.PostAsJsonAsync("/v1/chat/completions", request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(cancellationToken); + + return new ChatResponse(new ChatMessage(ChatRole.Assistant, result.Content)); + } + + public async IAsyncEnumerable CompleteStreamingAsync( + IEnumerable messages, + AICompletionContext context, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Similar to CompleteAsync but reads Server-Sent Events (SSE) + // and yields ChatResponseUpdate for each token + var client = _httpClientFactory.CreateClient("MyProvider"); + + // Build request with stream: true + var request = new + { + model = context.Deployment.ModelName, + messages = messages.Select(m => new { role = m.Role.Value, content = m.Text }), + stream = true, + }; + + using var response = await client.PostAsJsonAsync("/v1/chat/completions", request, cancellationToken); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + using var reader = new StreamReader(stream); + + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(cancellationToken); + if (string.IsNullOrEmpty(line) || !line.StartsWith("data: ")) + { + continue; + } + + var data = line["data: ".Length..]; + if (data == "[DONE]") + { + break; + } + + var chunk = JsonSerializer.Deserialize(data); + if (!string.IsNullOrEmpty(chunk?.Delta?.Content)) + { + yield return new ChatResponseUpdate + { + Text = chunk.Delta.Content, + }; + } + } + } +} +``` + +### Registering the Provider + +```csharp +services.AddScoped(); +``` + +The `IAIClientFactory` uses the `Name` property to route requests to the correct provider. When a deployment's provider connection references `"MyProvider"`, the factory creates a client using your implementation. + +## Example + +```csharp +// Inject the high-level service +public class MyService(IAICompletionService completionService) +{ + public async Task AskAsync(string question, AIDeployment deployment) + { + var messages = new List + { + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, question), + }; + + var response = await completionService.CompleteAsync(deployment, messages); + return response.Text; + } +} +``` + + diff --git a/src/CrestApps.Core.Docs/docs/core/ai-documents.md b/src/CrestApps.Core.Docs/docs/core/ai-documents.md new file mode 100644 index 00000000..128485ca --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/core/ai-documents.md @@ -0,0 +1,529 @@ +--- +sidebar_label: AI Documents +sidebar_position: 14 +title: AI Documents +description: Upload, process, chunk, embed, and search documents so the AI model can retrieve relevant content during conversations (RAG). +--- + +# AI Documents + +> A complete document management pipeline that reads uploaded files, splits them into chunks, generates vector embeddings, and makes the content searchable via semantic similarity — enabling retrieval-augmented generation (RAG) in AI conversations. + +## Quick Start + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration() + .AddCoreAIChatInteractions() + .AddCoreAIDocumentProcessing() + .AddCoreAIOpenAI(); + +// Register document and chunk stores +builder.Services.AddScoped(); +builder.Services.AddScoped(); +``` + +Upload a file and process it: + +```csharp +public sealed class DocumentUploadController( + IAIDocumentProcessingService processingService, + IAIDocumentStore documentStore) : Controller +{ + [HttpPost] + public async Task Upload( + IFormFile file, + string referenceId, + string referenceType) + { + var embeddingGenerator = + await processingService.CreateEmbeddingGeneratorAsync("OpenAI", "default"); + + var result = await processingService.ProcessFileAsync( + file, referenceId, referenceType, embeddingGenerator); + + return result.Succeeded ? Ok(result) : BadRequest(result); + } +} +``` + +## Problem & Solution + +Users upload documents (PDFs, Word files, spreadsheets, text files) and expect the AI to answer questions about them. This requires a multi-stage pipeline: + +- **Reading** — Extract plain text from diverse file formats (`.pdf`, `.docx`, `.xlsx`, `.csv`, `.txt`, `.md`, and more) +- **Chunking** — Split large documents into segments small enough to embed +- **Embedding** — Convert each chunk into a vector representation using a configured embedding model +- **Indexing** — Store embeddings in a vector search index (Elasticsearch or Azure AI Search) +- **Searching** — At query time, perform semantic similarity search to find the most relevant chunks +- **Tabular processing** — CSV and Excel files receive special treatment with structured, batch-oriented queries + +The document processing system handles this full pipeline from upload to retrieval, while the built-in document tools make the content available to the AI during orchestration. + +## Architecture Overview + +```text +┌─────────────┐ +│ User Upload │ +└──────┬──────┘ + ▼ +┌──────────────────────────────┐ +│ IAIDocumentProcessingService │ ← Orchestrates the pipeline +├──────────────────────────────┤ +│ 1. Store document record │ → IAIDocumentStore +│ 2. Read file content │ → IngestionDocumentReader (keyed by extension) +│ 3. Normalize & chunk text │ → RagTextNormalizer +│ 4. Store chunks │ → IAIDocumentChunkStore +│ 5. Generate embeddings │ → IEmbeddingGenerator> +│ 6. Index in vector store │ → ISearchDocumentManager (Elasticsearch / Azure AI) +└──────────────────────────────┘ + + ┌─────────────────────────────────────┐ + │ During Conversation │ + ├─────────────────────────────────────┤ + │ DocumentOrchestrationHandler │ + │ detects documents on the session │ + │ and injects document tools: │ + │ │ + │ • SearchDocumentsTool (vector RAG) │ + │ • ReadDocumentTool (full text read) │ + │ • ReadTabularDataTool (CSV/Excel) │ + └──────────────┬──────────────────────┘ + ▼ + ┌─────────────────────────────────────┐ + │ AI Model calls tools as needed │ + │ to answer user questions about │ + │ the uploaded documents │ + └─────────────────────────────────────┘ +``` + +## Core Interfaces + +| Interface | Package | Purpose | +|-----------|---------|---------| +| `IAIDocumentStore` | `CrestApps.Core.AI` | CRUD for document records | +| `IAIDocumentChunkStore` | `CrestApps.Core.AI` | CRUD for document chunks | +| `IAIDocumentProcessingService` | `CrestApps.Core.AI.Chat` | Orchestrates file → chunk → embed → index | +| `ISearchDocumentManager` | `CrestApps.Core.AI` | Manages documents in the vector search index | +| `IVectorSearchService` | `CrestApps.Core.AI` | Performs vector similarity search at query time | +| `ITabularBatchProcessor` | `CrestApps.Core.AI.Chat` | Splits and processes CSV/Excel batch queries | +| `ITabularBatchResultCache` | `CrestApps.Core.AI.Chat` | Caches tabular query results | +| `IngestionDocumentReader` | `CrestApps.Core.AI` | Abstract base for format-specific file readers | + +## Document Processing Pipeline + +### Step 1 — Upload and Store + +When a file is uploaded, a new `AIDocument` record is created in `IAIDocumentStore`: + +```csharp +public sealed class AIDocument : CatalogItem +{ + public string ReferenceId { get; set; } // Owning resource (e.g., chat interaction ID) + public string ReferenceType { get; set; } // Resource type (e.g., "chatinteraction") + public string FileName { get; set; } // Original file name + public string ContentType { get; set; } // MIME type + public long FileSize { get; set; } // Size in bytes + public DateTime UploadedUtc { get; set; } // Upload timestamp +} +``` + +The `ReferenceId` and `ReferenceType` pair ties the document to an owning resource. Common reference types include: + +| Constant | Value | Meaning | +|----------|-------|---------| +| `AIReferenceTypes.Document.Profile` | `"profile"` | Document attached to an AI profile | +| `AIReferenceTypes.Document.ChatInteraction` | `"chatinteraction"` | Document attached to a chat interaction | +| `AIReferenceTypes.Document.ChatSession` | `"chatsession"` | Document attached to a chat session | + +Hosts can layer extra behavior on top of this shared pipeline, such as indexing uploaded chunks into Elasticsearch or Azure AI Search. That host-specific indexing should be treated as a secondary step: uploads should complete after persistence, while any slower or failure-prone indexing work runs independently so the document remains attached and the host can log indexing failures explicitly. + +### Step 2 — Read File Content + +An `IngestionDocumentReader` is resolved as a keyed service using the file extension. The reader extracts plain text from the file: + +```csharp +public abstract class IngestionDocumentReader +{ + public abstract Task ReadAsync( + Stream source, + string identifier, + string mediaType, + CancellationToken cancellationToken = default); +} +``` + +### Step 3 — Normalize and Chunk + +The extracted text is normalized (whitespace, encoding) and split into chunks. Each chunk becomes an `AIDocumentChunk`: + +```csharp +public sealed class AIDocumentChunk : CatalogItem +{ + public string AIDocumentId { get; set; } // Parent document ID + public string ReferenceId { get; set; } // Denormalized from parent + public string ReferenceType { get; set; } // Denormalized from parent + public string Content { get; set; } // Chunk text + public float[] Embedding { get; set; } // Vector embedding + public int Index { get; set; } // Chunk order within the document +} +``` + +The `ReferenceId` and `ReferenceType` are denormalized from the parent document for efficient query access without joins. + +### Step 4 — Generate Embeddings + +If the file extension is **embeddable** (see [Built-in Document Readers](#built-in-document-readers)), each chunk is converted to a vector via `IEmbeddingGenerator>`: + +```csharp +var embeddingGenerator = + await processingService.CreateEmbeddingGeneratorAsync("OpenAI", "default"); +``` + +The generator is created from the configured provider and connection. Embeddings are stored on the chunk itself (`Embedding` property) so they survive index rebuilds. + +### Step 5 — Index in Vector Store + +Chunks with embeddings are pushed to the search index via `ISearchDocumentManager`: + +```csharp +public interface ISearchDocumentManager +{ + Task AddOrUpdateAsync( + IIndexProfileInfo profile, + IReadOnlyCollection documents, + CancellationToken cancellationToken = default); + + Task DeleteAsync( + IIndexProfileInfo profile, + IEnumerable documentIds, + CancellationToken cancellationToken = default); + + Task DeleteAllAsync( + IIndexProfileInfo profile, + CancellationToken cancellationToken = default); +} +``` + +Implementations are registered as keyed services by provider name (e.g., `"Elasticsearch"`, `"AzureAISearch"`). + +### Step 6 — Query-Time Retrieval + +During a conversation, `SearchDocumentsTool` calls `IVectorSearchService` to find the most relevant chunks: + +```csharp +public interface IVectorSearchService +{ + Task> SearchAsync( + IIndexProfileInfo indexProfile, + float[] embedding, + string referenceId, + string referenceType, + int topN, + CancellationToken cancellationToken = default); +} +``` + +The user's query is embedded, and the resulting vector is compared against indexed chunks using cosine similarity. + +## Built-in Document Readers + +| Reader | Extensions | Embeddable | Notes | +|--------|-----------|------------|-------| +| `PlainTextIngestionDocumentReader` | `.txt`, `.md`, `.json`, `.xml`, `.html`, `.htm`, `.log`, `.yaml`, `.yml` | Yes | UTF-8 stream reader | +| `PlainTextIngestionDocumentReader` | `.csv` | No | Tabular — processed via `ReadTabularDataTool` | +| `OpenXmlIngestionDocumentReader` | `.docx`, `.pptx` | Yes | Uses `DocumentFormat.OpenXml` SDK | +| `OpenXmlIngestionDocumentReader` | `.xlsx` | No | Tabular — processed via `ReadTabularDataTool` | +| `PdfIngestionDocumentReader` | `.pdf` | Yes | Uses `UglyToad.PdfPig` with DocstrumBoundingBoxes | + +**Embeddable** means the content is chunked and vector-embedded for semantic search. **Non-embeddable** (tabular) formats are instead handled by the `ReadTabularDataTool` which reads and parses them directly. + +## Custom Document Reader + +Register a reader for additional file formats: + +```csharp +builder.Services.AddCrestAppsIngestionDocumentReader( + new ExtractorExtension(".rtf", embeddable: true)); +``` + +Implement the reader: + +```csharp +public sealed class RtfIngestionDocumentReader : IngestionDocumentReader +{ + public override async Task ReadAsync( + Stream source, + string identifier, + string mediaType, + CancellationToken cancellationToken = default) + { + // Parse the RTF stream into plain text + using var reader = new StreamReader(source); + var rawContent = await reader.ReadToEndAsync(cancellationToken); + var plainText = StripRtfFormatting(rawContent); + + return new IngestionDocument + { + Content = plainText, + Identifier = identifier, + }; + } +} +``` + +### `ExtractorExtension` + +The `ExtractorExtension` type defines a file extension and whether its content is embeddable: + +```csharp +public sealed class ExtractorExtension +{ + public string Extension { get; } // Normalized with leading dot (e.g., ".rtf") + public bool Embeddable { get; } // Whether embeddings should be generated + + public ExtractorExtension(string extension, bool embeddable = true); +} +``` + +There is an implicit conversion from `string` to `ExtractorExtension` (with `embeddable: true` by default), so you can pass bare strings for embeddable extensions: + +```csharp +// These are equivalent: +services.AddCrestAppsIngestionDocumentReader(".rtf"); +services.AddCrestAppsIngestionDocumentReader(new ExtractorExtension(".rtf", true)); + +// For non-embeddable extensions, use the explicit constructor: +services.AddCrestAppsIngestionDocumentReader(new ExtractorExtension(".tsv", false)); +``` + +### `AddCrestAppsIngestionDocumentReader` + +```csharp +public static IServiceCollection AddCrestAppsIngestionDocumentReader( + this IServiceCollection services, + params ExtractorExtension[] supportedExtensions) + where T : IngestionDocumentReader; +``` + +This method: +1. Registers the reader as a singleton +2. Registers a keyed singleton for each extension (used to resolve the right reader at runtime) +3. Adds the extensions to `ChatDocumentsOptions` + +## Document Tools + +Three system tools are automatically available when documents are attached to a session. They are registered with `AIToolPurposes.DocumentProcessing` and injected by `DocumentOrchestrationHandler`. + +### `SearchDocumentsTool` + +**Name:** `search_documents` (`SystemToolNames.SearchDocuments`) + +Performs semantic vector search across all uploaded documents for the current session and returns the most relevant text chunks. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `query` | `string` | Yes | The search query to find relevant content | +| `top_n` | `integer` | No | Number of top matching chunks to return (default: 3) | + +### `ReadDocumentTool` + +**Name:** `read_document` (`SystemToolNames.ReadDocument`) + +Reads the full text content of a specific uploaded document. Truncates output to 50 KB maximum. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `document_id` | `string` | Yes | The unique identifier of the document to read | + +### `ReadTabularDataTool` + +**Name:** `read_tabular_data` (`SystemToolNames.ReadTabularData`) + +Reads tabular data from CSV, TSV, or Excel files and returns formatted rows suitable for analysis. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `document_id` | `string` | Yes | The unique identifier of the tabular document | +| `max_rows` | `integer` | No | Maximum number of data rows to return (default: 100) | + +**Supported extensions:** `.csv`, `.tsv`, `.xlsx`, `.xls` + +## Implementing Stores + +The framework defines two store interfaces. You must provide implementations for your persistence layer. + +### `IAIDocumentStore` + +```csharp +public interface IAIDocumentStore : ICatalog +{ + Task> GetDocumentsAsync( + string referenceId, + string referenceType); +} +``` + +Inherits CRUD operations from `ICatalog`: + +| Method | Description | +|--------|-------------| +| `CreateAsync(T)` | Insert a new document record | +| `UpdateAsync(T)` | Update an existing document record | +| `DeleteAsync(T)` | Delete a document record | +| `FindByIdAsync(string)` | Find a document by its `ItemId` | +| `GetAllAsync()` | Retrieve all documents | +| `GetAsync(IEnumerable)` | Retrieve documents by IDs | +| `PageAsync(int, int, TQuery)` | Paginated query | +| `SaveChangesAsync()` | Flush pending changes | + +### `IAIDocumentChunkStore` + +```csharp +public interface IAIDocumentChunkStore : ICatalog +{ + Task> GetChunksByAIDocumentIdAsync(string documentId); + Task> GetChunksByReferenceAsync( + string referenceId, string referenceType); + Task DeleteByDocumentIdAsync(string documentId); +} +``` + +### Registration + +Register your implementations with the DI container: + +```csharp +builder.Services.AddScoped(); +builder.Services.AddScoped(); +``` + +See [Data Storage](./data-storage.md) for more on the catalog pattern and YesSql index conventions. + +## Orchestration Integration + +`DocumentOrchestrationHandler` implements `IOrchestrationContextBuilderHandler` and is registered automatically by `AddCoreAIDocumentProcessing()`. + +```csharp +public sealed class DocumentOrchestrationHandler : IOrchestrationContextBuilderHandler +{ + public Task BuildingAsync(OrchestrationContextBuildingContext context); + public Task BuiltAsync(OrchestrationContextBuiltContext context); +} +``` + +During context building, the handler: + +1. Checks if the current session has documents (via `ReferenceId` / `ReferenceType`) +2. If documents exist, sets `AICompletionContextKeys.HasDocuments = true` +3. Discovers all tools with purpose `AIToolPurposes.DocumentProcessing` and adds them to the tool set +4. Enriches the system message with document metadata so the model knows what content is available + +This means document tools are **only** injected when the session actually has documents — no wasted tokens on tool descriptions when there are no documents. + +## Tabular Data + +CSV, TSV, and Excel files are marked as **non-embeddable** and receive special processing. + +### `ITabularBatchProcessor` + +Splits large tabular content into batches, processes each batch with the LLM, and merges results: + +```csharp +public interface ITabularBatchProcessor +{ + IList SplitIntoBatches(string content, string fileName); + + Task> ProcessBatchesAsync( + IList batches, + string userPrompt, + TabularBatchContext context, + CancellationToken cancellationToken = default); + + string MergeResults(IList results, bool includeHeader = true); +} +``` + +### `ITabularBatchResultCache` + +Caches batch results to avoid re-processing identical queries: + +```csharp +public interface ITabularBatchResultCache +{ + string GenerateCacheKey(string interactionId, string documentContentHash, string prompt); + string ComputeDocumentContentHash(IEnumerable<(string FileName, string Content)> documents); + TabularBatchCacheEntry TryGet(string cacheKey); + void Set(string cacheKey, TabularBatchCacheEntry entry, TimeSpan? expiration = null); + void Remove(string cacheKey); + void InvalidateForInteraction(string interactionId); +} +``` + +When documents are added or removed from an interaction, call `InvalidateForInteraction` to clear stale cache entries. + +## Configuration + +### `ChatDocumentsOptions` + +Controls which file types can be uploaded and how they are processed: + +```csharp +services.Configure(options => +{ + // Add an embeddable extension + options.Add(".rtf", embeddable: true); + + // Add a tabular (non-embeddable) extension + options.Add(".tsv", embeddable: false); +}); +``` + +| Property | Type | Description | +|----------|------|-------------| +| `AllowedFileExtensions` | `IReadOnlySet` | Complete set of uploadable file extensions | +| `EmbeddableFileExtensions` | `IReadOnlySet` | Subset that gets vector-embedded | + +Extensions not in `EmbeddableFileExtensions` are still allowed for upload and can be read by `ReadDocumentTool` or `ReadTabularDataTool`, but they are not chunked and embedded. + +### `InteractionDocumentSettings` + +Per-interaction settings for document search: + +```csharp +public sealed class InteractionDocumentSettings +{ + public string IndexProfileName { get; set; } // Index profile for embedding and search + public int TopN { get; set; } = 3; // Top matching chunks to include in context +} +``` + +### Limits + +- Maximum **25,000 characters** total for embedding per session +- `ReadDocumentTool` truncates output to **50 KB** +- `ReadTabularDataTool` defaults to **100 rows** maximum + +## Services Registered by `AddCoreAIDocumentProcessing()` + +| Service | Implementation | Lifetime | Purpose | +|---------|---------------|----------|---------| +| `IAIDocumentProcessingService` | `DefaultAIDocumentProcessingService` | Scoped | Orchestrates document processing | +| `ITabularBatchProcessor` | `TabularBatchProcessor` | Scoped | Processes CSV/Excel batch queries | +| `ITabularBatchResultCache` | `TabularBatchResultCache` | Singleton | Caches tabular query results | +| `DocumentOrchestrationHandler` | — | Scoped | Injects document context into orchestration | +| `PlainTextIngestionDocumentReader` | — | Singleton | `.txt`, `.csv`, `.md`, `.json`, `.xml`, `.html`, `.htm`, `.log`, `.yaml`, `.yml` | +| `OpenXmlIngestionDocumentReader` | — | Singleton | `.docx`, `.xlsx`, `.pptx` | +| `PdfIngestionDocumentReader` | — | Singleton | `.pdf` | +| `SearchDocumentsTool` | — | System tool | Semantic vector search | +| `ReadDocumentTool` | — | System tool | Full document read | +| `ReadTabularDataTool` | — | System tool | Tabular data queries | + + diff --git a/src/CrestApps.Core.Docs/docs/core/ai-memory.md b/src/CrestApps.Core.Docs/docs/core/ai-memory.md new file mode 100644 index 00000000..30983796 --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/core/ai-memory.md @@ -0,0 +1,96 @@ +--- +sidebar_label: AI Memory +sidebar_position: 13 +title: AI Memory +description: Long-term memory services for user-scoped facts, semantic retrieval, and memory-aware orchestration. +--- + +# AI Memory + +`CrestApps.Core` includes reusable memory services for applications that want an AI assistant to remember durable user facts across sessions. + + +## What the framework provides + +`AddCoreAIMemory()` adds the shared runtime behavior for: + +- memory tool registration +- safety validation for memory writes +- semantic memory search orchestration +- preemptive memory retrieval during orchestration +- shared indexing and search helpers + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration() + .AddCoreAIMemory() + .AddCoreAIOpenAI(); +``` + +## What your host must provide + +The framework does not assume a single persistence model. A host application is responsible for wiring the storage and search pieces that match its runtime: + +- an `IAIMemoryStore` implementation for durable memory entries +- an `ISearchIndexProfileStore` implementation for index profile lookup +- one or more keyed `IMemoryVectorSearchService` implementations +- options such as `AIMemoryOptions`, `GeneralAIOptions`, and `ChatInteractionMemoryOptions` + + +## Core concepts + +### Memory entries + +A memory entry is a durable user-scoped fact: + +| Field | Purpose | +| --- | --- | +| `UserId` | Identifies the owner of the memory | +| `Name` | Stable key such as `preferred-language` | +| `Description` | Semantic summary used to improve retrieval quality | +| `Content` | The value to retain for later recall | +| `CreatedUtc` / `UpdatedUtc` | Lifecycle timestamps | + +### Safety validation + +Before a memory is stored, `IAIMemorySafetyService` can reject obviously sensitive data such as credentials, connection strings, SSNs, or payment card numbers. The framework ships with the validation pipeline; hosts decide how they surface validation failures. + +### User scoping + +Memory tools operate on the current authenticated user. The framework resolves identity from orchestration scope or the current HTTP context so retrieval stays user-specific. + +## Key contracts + +| Contract | Purpose | +| --- | --- | +| `IAIMemoryStore` | CRUD and query access for persisted memory entries | +| `IAIMemorySearchService` | Shared semantic retrieval over memory entries | +| `IMemoryVectorSearchService` | Provider-specific vector search adapter | +| `IAIMemorySafetyService` | Validation for writes before they are stored | +| `IPreemptiveRagHandler` | Injects relevant memory context before the model responds | + +## Built-in tools + +When memory is enabled, the orchestration layer can expose these system tools: + +| Tool | Purpose | +| --- | --- | +| `save_user_memory` | Create or update a durable memory | +| `search_user_memories` | Find relevant memories by semantic similarity | +| `list_user_memories` | Enumerate saved memories for the current user | +| `remove_user_memory` | Delete a saved memory by name | + +These tools are intended for long-lived facts such as preferences, recurring projects, or roles, not for transient one-off chat state. + +## Typical flow + +1. Register `AddCoreAIMemory()` with the rest of the AI runtime. +2. Provide the store, vector search, and option bindings for your host. +3. Enable memory-aware orchestration for the profiles or chat surfaces that should use it. +4. Let the orchestrator decide when to store, search, or inject memory context. + +## Related guidance + +- Pair memory with **[Orchestration](./orchestration.md)** when you want automatic recall +- Pair memory with **[Data Sources](../data-sources/index.md)** when you also need document or index-backed RAG diff --git a/src/CrestApps.Core.Docs/docs/core/ai-templates.md b/src/CrestApps.Core.Docs/docs/core/ai-templates.md new file mode 100644 index 00000000..e731f843 --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/core/ai-templates.md @@ -0,0 +1,420 @@ +--- +sidebar_label: AI Templates +sidebar_position: 7 +title: AI Templates +description: Liquid-based prompt template engine for managing, rendering, and composing AI system prompts. +--- + +# AI Templates + +> A Liquid-based template engine for managing, rendering, and composing AI system prompts from multiple sources. + +## Quick Start + +```csharp +builder.Services.AddCoreAITemplating(); +``` + +:::info +You rarely need to call this directly — `AddCoreAIServices()` chains it automatically. +::: + +## Problem & Solution + +Hard-coding system prompts in C# makes them difficult to maintain, localize, and customize. The template system: + +- Stores prompts as **markdown files** with front-matter metadata +- Renders them with **Liquid** syntax for dynamic content +- Discovers templates from **multiple sources** (embedded resources, file system, code) +- Supports **merging** multiple templates into a single prompt + +## Services Registered by `AddCoreAITemplating()` + +`AddCoreAITemplating()` builds on the lower-level `AddTemplating()` registration and also adds the built-in AI template source metadata for `SystemPrompt` and `Profile` templates. + +| Service | Implementation | Lifetime | Purpose | +|---------|---------------|----------|---------| +| `ITemplateParser` | `DefaultMarkdownTemplateParser` | Singleton | Parses markdown front-matter templates | +| `ITemplateEngine` | `FluidTemplateEngine` | Singleton | Renders Liquid templates | +| `ITemplateService` | `DefaultTemplateService` | Scoped | Unified template discovery and rendering | +| `OptionsTemplateProvider` | — | Singleton | Templates registered via code | +| `FileSystemTemplateProvider` | — | Singleton | Templates discovered from disk | + +## Key Interfaces + +### `ITemplateService` + +The main service for working with templates. + +```csharp +public interface ITemplateService +{ + Task> ListAsync(); + Task