From f638b06a869a8af53171d436d8fc764b17dbc909 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Mon, 4 May 2026 22:05:10 +0200 Subject: [PATCH] initial commit --- .eslintrc.js | 23 + .gitignore | 35 + .prettierrc | 19 + CHANGES.md | 38 + README.md | 65 + __mocks__/obsidian.ts | 50 + __mocks__/ollama-client.ts | 32 + coverage/lcov-report/base.css | 224 + coverage/lcov-report/block-navigation.js | 87 + coverage/lcov-report/chat-view.ts.html | 1381 +++++ coverage/lcov-report/error-handler.ts.html | 619 ++ coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes coverage/lcov-report/index.html | 116 + coverage/lcov-report/ollama-client.ts.html | 706 +++ coverage/lcov-report/prettify.css | 1 + coverage/lcov-report/prettify.js | 2 + coverage/lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/lcov-report/sorter.js | 210 + coverage/lcov-report/tool-executor.ts.html | 265 + coverage/lcov-report/types.ts.html | 487 ++ coverage/lcov-report/utils.ts.html | 241 + coverage/lcov-report/vault-indexer.ts.html | 1276 ++++ coverage/lcov.info | 188 + jest.config.js | 18 + jest.setup.js | 0 main.ts | 77 + package-lock.json | 6274 ++++++++++++++++++++ package.json | 38 + rules | 111 + src/chat-view.ts | 432 ++ src/error-handler.ts | 178 + src/ollama-client.ts | 207 + src/tool-executor.ts | 60 + src/types.ts | 134 + src/utils.ts | 52 + src/vault-indexer.ts | 397 ++ tests/chat-view.test.ts | 211 + tests/error-handler.test.ts | 191 + tests/ollama-client.test.ts | 368 ++ tests/tool-executor.test.ts | 429 ++ tests/vault-indexer.test.ts | 399 ++ tsconfig.json | 20 + tsconfig.test.json | 9 + 43 files changed, 15670 insertions(+) create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 CHANGES.md create mode 100644 README.md create mode 100644 __mocks__/obsidian.ts create mode 100644 __mocks__/ollama-client.ts create mode 100644 coverage/lcov-report/base.css create mode 100644 coverage/lcov-report/block-navigation.js create mode 100644 coverage/lcov-report/chat-view.ts.html create mode 100644 coverage/lcov-report/error-handler.ts.html create mode 100644 coverage/lcov-report/favicon.png create mode 100644 coverage/lcov-report/index.html create mode 100644 coverage/lcov-report/ollama-client.ts.html create mode 100644 coverage/lcov-report/prettify.css create mode 100644 coverage/lcov-report/prettify.js create mode 100644 coverage/lcov-report/sort-arrow-sprite.png create mode 100644 coverage/lcov-report/sorter.js create mode 100644 coverage/lcov-report/tool-executor.ts.html create mode 100644 coverage/lcov-report/types.ts.html create mode 100644 coverage/lcov-report/utils.ts.html create mode 100644 coverage/lcov-report/vault-indexer.ts.html create mode 100644 coverage/lcov.info create mode 100644 jest.config.js create mode 100644 jest.setup.js create mode 100644 main.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 rules create mode 100644 src/chat-view.ts create mode 100644 src/error-handler.ts create mode 100644 src/ollama-client.ts create mode 100644 src/tool-executor.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts create mode 100644 src/vault-indexer.ts create mode 100644 tests/chat-view.test.ts create mode 100644 tests/error-handler.test.ts create mode 100644 tests/ollama-client.test.ts create mode 100644 tests/tool-executor.test.ts create mode 100644 tests/vault-indexer.test.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..f9e141a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,23 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], + rules: { + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'no-console': 'warn', + 'no-empty': ['error', { allowEmptyCatch: true }], + }, + env: { + node: true, + es2020: true, + jest: true, + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b96262 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules/ + +# Built files +lib/ +dist/ +out/ +build/ + +# IDE files +.idea/ +.vscode/ +*.suo +*.ntvs* +*.njsproj +*.sln + +# Backup files +*.backup + +# Log files +*.log +npm-debug.log* + +# Runtime data +.pnp.* +.yarn/ + +# Editor settings +.DS_Store +*.swp +*.swo + +# Test cache +.jest-cache/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..802f945 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,19 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "es5", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "always", + "endOfLine": "auto", + "overrides": [ + { + "files": "*.{md,markdown}", + "options": { + "printWidth": 80 + } + } + ] +} diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..84b6170 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,38 @@ +# Changes + +## [Unreleased] + +### Fixed + +- **CRITICAL**: Fixed `removeEventListeners` in `chat-view.ts` which was incorrectly adding listeners instead of removing them, causing memory leaks and duplicate event handlers +- **CRITICAL**: Added guards in `setupEventListeners` to prevent duplicate event listeners from accumulating on repeated open/close cycles +- **CRITICAL**: Added path sanitization in `tool-executor.ts` to prevent path traversal attacks by validating that paths are relative and don't contain `..` segments +- **MAJOR**: Added proper cleanup in `ollama-client.ts` async generator with try/finally block to ensure `reader.releaseLock()` and abort controller cleanup on parse errors +- **MAJOR**: Made `fetchFn` injectable in `OllamaClient` constructor to improve testability + +### Added + +- **MAJOR**: Added conversation history management with "New Chat" button to clear messages and start fresh conversations +- **MINOR**: Added `.gitignore` file to exclude backup files, build artifacts, and other unwanted files +- **MINOR**: Created `README.md` with setup instructions and usage documentation +- **MINOR**: Added sensible default values for Ollama URL (`http://localhost:11434`) and model (`llama3`) to prevent confusing failures +- **NIT**: Added `CHANGES.md` file to document changes + +### Changed + +- **MINOR**: Fixed `tsconfig.json` to include `main.ts` in the root directory instead of incorrectly scoping to `./src` +- **NIT**: Refactored code formatting and structure for better readability + +### Security + +- Added path validation to prevent directory traversal attacks in file creation tool +- Added content type validation to ensure proper argument parsing + +### Technical Debt + +- Removed committed backup file (`src/chat-view.ts.backup`) +- Removed references to hardcoded paths and improved modularity + +## [Previous Versions] + +No formal versioning was maintained prior to these changes. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e983eed --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Ollama Chat Plugin for Obsidian + +A plugin that integrates Ollama with Obsidian to create a chat interface that can access your vault content. + +## Features + +- Chat with Ollama models directly in Obsidian +- Vault context search - the assistant can reference your notes +- Tool integration - create files based on chat responses +- Streaming responses +- Customizable model and URL settings + +## Installation + +1. Install the plugin via Obsidian's community plugins +2. Make sure you have Ollama installed and running + +## Setup + +1. **Install Ollama**: Follow the instructions at [ollama.ai](https://ollama.ai) to install Ollama +2. **Start Ollama service**: `ollama serve` +3. **Pull a model**: `ollama pull llama3` (or any other model you prefer) + +## Configuration + +1. Open the plugin settings via Obsidian's settings panel +2. Configure the Ollama URL (default: `http://localhost:11434`) +3. Configure the model name (default: `llama3`) +4. Restart the plugin if needed + +## Usage + +1. Click the ribbon icon to open the chat view +2. Type your message in the input box +3. Press Enter or click Send to send your message +4. Click the "New Chat" button to start a fresh conversation + +## Supported Models + +Any model supported by Ollama should work, including: + +- llama3 +- llama2 +- mistral +- codellama +- etc. + +## Development + +To build from source: + +```bash +npm install +npm run build +``` + +## Troubleshooting + +- **Connection issues**: Ensure Ollama is running and accessible at the configured URL +- **Model not found**: Make sure you've pulled the model (`ollama pull `) +- **Permission issues**: Check that your Obsidian vault has proper write permissions + +## License + +MIT diff --git a/__mocks__/obsidian.ts b/__mocks__/obsidian.ts new file mode 100644 index 0000000..551521e --- /dev/null +++ b/__mocks__/obsidian.ts @@ -0,0 +1,50 @@ +// Enhanced Obsidian mock for testing +export class Vault { + getMarkdownFiles() { + return []; + } + + async read(file: any) { + return ''; + } + + async create(path: string, content: string) { + return null; + } +} + +export class Workspace { + getLeaf() { + return { + setViewState: jest.fn(), + }; + } +} + +export class App { + vault = new Vault(); + workspace = new Workspace(); +} + +export class ItemView { + contentEl: HTMLElement = document.createElement('div'); + app: App; + + constructor() { + this.app = new App(); + } +} + +export class Notice { + static create(message: string) {} +} + +// Mock types for DOM elements +export type TFile = { + basename: string; +}; + +// Export additional types that might be used in tests +export const Plugin: any = jest.fn(); +export const WorkspaceLeaf: any = jest.fn(); +export const Setting: any = jest.fn(); diff --git a/__mocks__/ollama-client.ts b/__mocks__/ollama-client.ts new file mode 100644 index 0000000..b366229 --- /dev/null +++ b/__mocks__/ollama-client.ts @@ -0,0 +1,32 @@ +// Mock for ollama-client for testing +import { OllamaMessage, OllamaTool, ToolCall } from '../src/types'; + +export class OllamaClient { + private url: string; + private model: string; + + // Mock fetch function for testing + private fetchFn: typeof fetch = jest.fn(); + + constructor(url: string, model: string, fetchFn?: typeof fetch) { + this.url = url; + this.model = model; + if (fetchFn) this.fetchFn = fetchFn; + } + + async streamChatMessages( + prompt: string, + options: { abortSignal?: AbortSignal } = {} + ): Promise { + // Mock implementation - return a simple response + return `Mock response for: ${prompt}`; + } + + async streamToolMessages( + toolCall: string, + options: { abortSignal?: AbortSignal } = {} + ): Promise { + // Mock implementation - return a simple tool response + return `Mock tool response for: ${toolCall}`; + } +} diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/chat-view.ts.html b/coverage/lcov-report/chat-view.ts.html new file mode 100644 index 0000000..fb5ca7e --- /dev/null +++ b/coverage/lcov-report/chat-view.ts.html @@ -0,0 +1,1381 @@ + + + + + + Code coverage report for chat-view.ts + + + + + + + + + +
+
+

All files chat-view.ts

+
+ +
+ 83.07% + Statements + 162/195 +
+ + +
+ 72.3% + Branches + 47/65 +
+ + +
+ 73.07% + Functions + 19/26 +
+ + +
+ 86.18% + Lines + 156/181 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +4331x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +1x +1x +1x +1x +  +1x +  +11x +  +  +  +11x +11x +11x +11x +11x +11x +11x +11x +11x +  +  +11x +11x +11x +11x +11x +  +  +  +1x +  +  +  +1x +  +  +  +3x +3x +3x +  +  +  +2x +2x +2x +2x +2x +2x +2x +  +  +  +  +4x +2x +2x +  +  +  +  +  +11x +11x +  +11x +  +  +11x +4x +  +11x +4x +  +  +4x +  +  +11x +  +8x +  +8x +  +  +8x +8x +  +  +  +11x +  +  +11x +  +  +11x +11x +  +11x +7x +  +  +7x +2x +  +5x +  +  +5x +5x +  +  +  +  +11x +2x +2x +  +  +  +  +  +11x +11x +  +  +  +  +  +  +  +  +  +  +3x +  +  +3x +3x +  +  +  +  +  +  +3x +3x +  +  +  +  +  +  +  +  +3x +3x +3x +3x +3x +  +3x +  +  +  +  +3x +  +  +  +5x +1x +  +  +  +  +5x +1x +  +  +  +  +5x +1x +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +6x +3x +3x +  +  +  +  +3x +  +  +  +6x +3x +3x +  +  +3x +  +3x +3x +  +  +  +  +2x +2x +  +2x +  +2x +2x +  +  +2x +2x +  +  +2x +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +2x +2x +  +  +2x +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +2x +  +2x +  +2x +2x +2x +2x +2x +2x +  +2x +2x +2x +2x +  +  +  +2x +2x +  +  +2x +1x +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +2x +  +1x +1x +  +  +  +  +1x +1x +  +  +1x +1x +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +1x +  +  +  +2x +1x +1x +1x +1x +  +  +  +  +2x +  +  +2x +  +  +  +  +  +2x +2x +  +  +  +  + 
import { ItemView, WorkspaceLeaf, Notice, TFile } from 'obsidian';
+/// <reference lib="dom" />
+// Use global types from JSDOM setup
+type KeyboardEvent = globalThis.KeyboardEvent;
+type HTMLTextAreaElement = globalThis.HTMLTextAreaElement;
+type HTMLButtonElement = globalThis.HTMLButtonElement;
+ 
+const DEFAULT_VAULT_SEARCH_LIMIT = 3;
+const MAX_MESSAGE_HISTORY = 50;
+import {
+  PluginSettings,
+  OllamaMessage,
+  ChatMessage,
+  OllamaTool,
+  ToolCall,
+  ToolResult,
+} from './types';
+import { OllamaClient } from './ollama-client';
+import { VaultIndexer } from './vault-indexer';
+import { ToolExecutor } from './tool-executor';
+import { ErrorHandler } from './error-handler';
+ 
+export class ChatView extends ItemView {
+  private settings: PluginSettings;
+  private messages: ChatMessage[] = [];
+  private ollamaClient: OllamaClient;
+  private vaultIndexer: VaultIndexer;
+  private toolExecutor: ToolExecutor;
+  private lastMessageEl: HTMLElement | null = null;
+  private newChatButton: HTMLElement | null = null;
+  private sendButton: HTMLElement | null = null;
+  private inputEl: HTMLElement | null = null;
+  private chatContainer: HTMLElement | null = null;
+  private sendButtonClickHandler: (() => Promise<void>) | null = null;
+  private inputKeyDownHandler: ((e: KeyboardEvent) => Promise<void>) | null = null;
+  private newChatButtonClickHandler: (() => void) | null = null;
+  private listenersAttached = false;
+ 
+  constructor(leaf: WorkspaceLeaf, settings: PluginSettings) {
+    super(leaf);
+    this.settings = settings;
+    this.ollamaClient = new OllamaClient(settings.ollamaUrl, settings.model);
+    this.vaultIndexer = new VaultIndexer(this.app.vault);
+    this.toolExecutor = new ToolExecutor(this.app.vault, this.app);
+  }
+ 
+  getViewType(): string {
+    return 'ollama-chat-view';
+  }
+ 
+  getDisplayText(): string {
+    return 'Ollama Chat';
+  }
+ 
+  async onOpen() {
+    await this.render();
+    this.removeEventListeners(); // Clean up any existing listeners before reattaching
+    this.setupEventListeners();
+  }
+ 
+  async onClose() {
+    this.ollamaClient.cancelStream();
+    this.removeEventListeners();
+    this.cleanupStreamingResources();
+    this.lastMessageEl = null;
+    this.sendButton = null;
+    this.inputEl = null;
+    this.chatContainer = null;
+  }
+ 
+  private cleanupStreamingResources(): void {
+    // Ensure any ongoing streaming is properly cleaned up
+    if (this.lastMessageEl && this.lastMessageEl.parentElement) {
+      this.lastMessageEl.parentElement.removeChild(this.lastMessageEl);
+      this.lastMessageEl = null;
+    }
+  }
+ 
+  async render() {
+    const container =
+      this.chatContainer || this.contentEl.createEl('div', { cls: 'ollama-chat-container' });
+    this.chatContainer = container;
+    const inputContainer =
+      this.contentEl.querySelector('.ollama-input-container') ||
+      this.contentEl.createEl('div', { cls: 'ollama-input-container' });
+ 
+    if (!this.inputEl) {
+      this.inputEl = inputContainer.createEl('textarea', { cls: 'ollama-input' });
+    }
+    if (!this.sendButton) {
+      this.sendButton = inputContainer.createEl('button', {
+        cls: 'ollama-send-button',
+      }) as HTMLButtonElement;
+      (this.sendButton as HTMLButtonElement).textContent = 'Send';
+    }
+ 
+    if (!this.newChatButton) {
+      const newChatContainer =
+        this.contentEl.querySelector('.ollama-new-chat') ||
+        this.contentEl.createEl('div', { cls: 'ollama-new-chat' });
+      this.newChatButton = newChatContainer.createEl('button', {
+        cls: 'ollama-new-chat-button',
+      }) as HTMLButtonElement;
+      (this.newChatButton as HTMLButtonElement).textContent = '🔄 New Chat';
+      (this.newChatButton as HTMLButtonElement).title = 'Start a new conversation';
+    }
+ 
+    // Create immutable snapshot for rendering
+    const messagesSnapshot = [...this.messages];
+ 
+    // Only render messages that are not currently streaming
+    const nonStreamingMessages = messagesSnapshot.filter((msg) => !msg.isStreaming);
+ 
+    // Differential update: only update messages that have changed
+    const existingMessages = container.querySelectorAll('.ollama-message');
+    const existingIds = Array.from(existingMessages).map((el) => el.getAttribute('data-msg-id'));
+ 
+    for (const msg of nonStreamingMessages) {
+      const existingEl = container.querySelector(
+        `.ollama-message[data-msg-id="${msg.id}"]`
+      ) as HTMLElement | null;
+      if (existingEl) {
+        existingEl.textContent = msg.content;
+      } else {
+        const messageEl = container.createEl('div', {
+          cls: `ollama-message ${msg.role}`,
+        }) as HTMLElement;
+        messageEl.setAttribute('data-msg-id', msg.id);
+        messageEl.textContent = msg.content;
+      }
+    }
+ 
+    // Remove messages that are no longer in the array
+    for (const el of Array.from(existingMessages)) {
+      const id = el.getAttribute('data-msg-id');
+      Iif (!id || !nonStreamingMessages.some((m) => m.id === id)) {
+        el.remove();
+      }
+    }
+ 
+    // Re-attach streaming message if it exists
+    const streamingMessage = messagesSnapshot.find((msg) => msg.isStreaming);
+    Iif (streamingMessage && this.lastMessageEl) {
+      const existingStreamingEl = container.querySelector(
+        `.ollama-message[data-msg-id="${streamingMessage.id}"]`
+      );
+      Iif (!existingStreamingEl) {
+        container.appendChild(this.lastMessageEl);
+      }
+    }
+  }
+ 
+  private setupEventListeners(): void {
+    Iif (!this.sendButton || !this.inputEl || this.listenersAttached) return;
+ 
+    // Create handlers if they don't exist
+    if (!this.sendButtonClickHandler) {
+      this.sendButtonClickHandler = async () => {
+        Iif (!this.inputEl) return;
+        await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value);
+        (this.inputEl as HTMLTextAreaElement).value = '';
+      };
+    }
+ 
+    if (!this.inputKeyDownHandler) {
+      this.inputKeyDownHandler = async (e: KeyboardEvent) => {
+        Iif (!this.inputEl || e.key !== 'Enter' || e.shiftKey) return;
+        e.preventDefault();
+        await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value);
+        (this.inputEl as HTMLTextAreaElement).value = '';
+      };
+    }
+ 
+    // Add event listeners
+    (this.sendButton as HTMLButtonElement).addEventListener('click', this.sendButtonClickHandler!);
+    (this.inputEl as HTMLTextAreaElement).addEventListener('keydown', this.inputKeyDownHandler!);
+    if (this.newChatButton) {
+      if (!this.newChatButtonClickHandler) {
+        this.newChatButtonClickHandler = () => this.clearConversation();
+      }
+      (this.newChatButton as HTMLButtonElement).addEventListener(
+        'click',
+        this.newChatButtonClickHandler!
+      );
+    }
+    this.listenersAttached = true;
+  }
+ 
+  private removeEventListeners(): void {
+    if (this.sendButton && this.sendButtonClickHandler) {
+      (this.sendButton as HTMLButtonElement).removeEventListener(
+        'click',
+        this.sendButtonClickHandler!
+      );
+    }
+    if (this.inputEl && this.inputKeyDownHandler) {
+      (this.inputEl as HTMLTextAreaElement).removeEventListener(
+        'keydown',
+        this.inputKeyDownHandler!
+      );
+    }
+    if (this.newChatButton && this.newChatButtonClickHandler) {
+      (this.newChatButton as HTMLButtonElement).removeEventListener(
+        'click',
+        this.newChatButtonClickHandler!
+      );
+    }
+    this.listenersAttached = false;
+  }
+ 
+  private clearConversation(): void {
+    // Create new array to ensure immutability
+    this.messages = [];
+    this.lastMessageEl = null;
+    this.render();
+    new Notice('Conversation cleared');
+  }
+ 
+  private updateMessageById(id: string, partial: Partial<ChatMessage>): boolean {
+    const index = this.messages.findIndex((m) => m.id === id);
+    Iif (index < 0) return false;
+    this.messages = [
+      ...this.messages.slice(0, index),
+      { ...this.messages[index], ...partial },
+      ...this.messages.slice(index + 1),
+    ];
+    return true;
+  }
+ 
+  private async updateLastMessage(content: string) {
+    const streamingMessage = this.messages.find((msg) => msg.isStreaming);
+    if (streamingMessage && !this.lastMessageEl) {
+      this.lastMessageEl = this.contentEl.createEl('div', {
+        cls: `ollama-message assistant`,
+      }) as HTMLElement;
+      this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
+    }
+    if (this.lastMessageEl) {
+      this.lastMessageEl.textContent = content;
+    }
+  }
+ 
+  private async handleUserInput(content: string) {
+    Iif (!this.sendButton || !this.inputEl) return;
+    (this.sendButton as HTMLButtonElement).disabled = true;
+ 
+    try {
+      // Guard against empty messages
+      const userMessage = content.trim();
+      Iif (!userMessage) return;
+ 
+      // Search vault using user message as query
+      const entries = await this.vaultIndexer.searchVault(userMessage, DEFAULT_VAULT_SEARCH_LIMIT);
+      let context = entries.map((e) => `### ${e.title}\n${e.content}`).join('\n\n');
+ 
+      // Cap context size to prevent prompt bloat with large vaults
+      const MAX_CONTEXT_LENGTH = 4000;
+      Iif (context.length > MAX_CONTEXT_LENGTH) {
+        context = context.substring(0, MAX_CONTEXT_LENGTH) + '\n\n... (truncated)';
+      }
+ 
+      const systemMessage: OllamaMessage = {
+        role: 'system',
+        content: 'You are a helpful assistant.',
+      };
+      const userMessageWithContext: OllamaMessage = {
+        role: 'user',
+        content: `${context}\n\n${userMessage}`,
+      };
+ 
+      const messages: OllamaMessage[] = [
+        systemMessage,
+        ...this.messages.map(
+          (m) =>
+            ({
+              role: m.role,
+              content: m.content,
+              tool_calls: m.tool_calls,
+            }) as OllamaMessage
+        ),
+        userMessageWithContext,
+      ];
+ 
+      const tools: OllamaTool[] = [
+        {
+          type: 'function',
+          function: {
+            name: 'create_file',
+            description: 'Create a new file in the vault',
+            parameters: {
+              type: 'object' as const,
+              properties: {
+                path: { type: 'string' as const },
+                content: { type: 'string' as const },
+              },
+              required: ['path', 'content'],
+            },
+          },
+        },
+      ];
+ 
+      const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+ 
+      const userMessageId = messageId;
+      const assistantMessageId = `${messageId}-assistant`;
+ 
+      // Store user message in conversation history
+      const userChatMessage: ChatMessage = {
+        id: userMessageId,
+        role: 'user' as const,
+        content: userMessage,
+        timestamp: Date.now(),
+      };
+ 
+      const assistantMessage: ChatMessage = {
+        id: assistantMessageId,
+        role: 'assistant' as const,
+        content: '',
+        timestamp: Date.now(),
+        isStreaming: true,
+      };
+ 
+      // Update messages immutably
+      this.messages = [...this.messages, userChatMessage, assistantMessage];
+ 
+      await this.render();
+ 
+      const stream = await this.ollamaClient.streamChat(messages, tools);
+      let fullResponse = '';
+      let toolCalls: ToolCall[] = [];
+      let chunkCount = 0;
+      const MAX_STREAM_CHUNKS = 1000;
+      const maxChunks = MAX_STREAM_CHUNKS;
+ 
+      try {
+        for await (const chunk of stream) {
+          chunkCount++;
+          Iif (chunkCount > maxChunks) {
+            throw new Error('Response too long, stopped streaming');
+          }
+ 
+          if (chunk.content) {
+            fullResponse += chunk.content;
+          }
+ 
+          if (chunk.tool_calls) {
+            toolCalls = toolCalls.concat(chunk.tool_calls);
+          }
+ 
+          await this.updateLastMessage(fullResponse);
+        }
+      } finally {
+        // Clean up streaming resources regardless of outcome
+        this.cleanupStreamingResources();
+      }
+ 
+      // Update the assistant message with the full response immutably
+      Iif (
+        !this.updateMessageById(assistantMessageId, {
+          content: fullResponse,
+          tool_calls: toolCalls,
+        })
+      ) {
+        throw new Error('Assistant message not found');
+      }
+ 
+      // Process tool calls with proper follow-up context
+      if (toolCalls.length > 0) {
+        // Validate tool calls before processing
+        const MAX_TOOL_CALLS = 10;
+        Iif (toolCalls.length > MAX_TOOL_CALLS) {
+          throw new Error(`Too many tool calls (max ${MAX_TOOL_CALLS})`);
+        }
+ 
+        // Collect all tool results using allSettled to support partial results
+        const settledResults = await Promise.allSettled(
+          toolCalls.map((call) => this.toolExecutor.handleToolCall(call))
+        );
+ 
+        let toolResults: ToolResult[] = [];
+        for (const result of settledResults) {
+          Iif (result.status === 'fulfilled') {
+            toolResults.push(result.value);
+          } else {
+            // Use centralized error handler for tool errors
+            ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput');
+          }
+        }
+ 
+        // Create follow-up messages including the assistant's tool calls and results
+        const followUpMessages: OllamaMessage[] = [
+          ...messages,
+          { role: 'assistant' as const, content: fullResponse, tool_calls: toolCalls },
+          ...toolResults.map((result) => ({
+            role: 'tool' as const,
+            content: JSON.stringify(result),
+          })),
+        ];
+ 
+        const followUp = await this.ollamaClient.chat(followUpMessages, tools);
+        fullResponse += followUp.content;
+        await this.updateLastMessage(fullResponse);
+ 
+        // Update the assistant message with the final response immutably
+        this.updateMessageById(assistantMessageId, { content: fullResponse, isStreaming: false });
+      }
+ 
+      // Update last message immutably — only if no tool calls were processed
+      if (toolCalls.length === 0) {
+        const lastMessageIndex = this.messages.length - 1;
+        if (lastMessageIndex >= 0) {
+          const lastMessage = { ...this.messages[lastMessageIndex], isStreaming: false };
+          this.messages = [...this.messages.slice(0, lastMessageIndex), lastMessage];
+        }
+      }
+ 
+      // Limit conversation history to prevent memory issues
+      Iif (this.messages.length > MAX_MESSAGE_HISTORY) {
+        this.messages = this.messages.slice(-MAX_MESSAGE_HISTORY);
+      }
+      await this.render();
+    } catch (error) {
+      // Use centralized error handler
+      ErrorHandler.handleError(error, 'ChatView.handleUserInput');
+      this.cleanupStreamingResources();
+    } finally {
+      if (this.sendButton) {
+        (this.sendButton as HTMLButtonElement).disabled = false;
+      }
+    }
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/error-handler.ts.html b/coverage/lcov-report/error-handler.ts.html new file mode 100644 index 0000000..a532ce9 --- /dev/null +++ b/coverage/lcov-report/error-handler.ts.html @@ -0,0 +1,619 @@ + + + + + + Code coverage report for error-handler.ts + + + + + + + + + +
+
+

All files error-handler.ts

+
+ +
+ 84.37% + Statements + 54/64 +
+ + +
+ 82% + Branches + 41/50 +
+ + +
+ 100% + Functions + 10/10 +
+ + +
+ 84.37% + Lines + 54/64 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +1792x +2x +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +5x +5x +  +5x +2x +2x +3x +2x +2x +  +1x +1x +  +  +5x +5x +  +  +  +5x +  +  +5x +4x +  +  +  +  +  +  +  +8x +  +2x +2x +  +  +  +  +  +  +  +  +  +  +2x +2x +2x +1x +  +1x +  +  +  +  +1x +1x +  +  +  +  +2x +2x +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +  +8x +2x +  +  +6x +  +  +  +  +1x +  +  +5x +1x +  +  +4x +1x +  +  +3x +1x +  +  +2x +1x +  +  +1x +  +  +  +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  + 
import { Notice } from 'obsidian';
+import {
+  OllamaError,
+  ErrorType,
+  NetworkError,
+  ApiError,
+  ValidationError,
+  StreamingError,
+  ToolExecutionError,
+  PathValidationError,
+} from './types';
+ 
+export class ErrorHandler {
+  /**
+   * Centralized error handling for the Ollama plugin
+   * Provides consistent error messages and logging
+   */
+  static handleError(error: unknown, context?: string): void {
+    let userMessage = 'An unexpected error occurred';
+    let shouldShowError = true;
+ 
+    if (error instanceof OllamaError) {
+      userMessage = this.getUserFriendlyMessage(error);
+      shouldShowError = true;
+    } else if (error instanceof Error) {
+      userMessage = this.getUserFriendlyMessageFromError(error);
+      shouldShowError = true;
+    } else {
+      userMessage = 'An unexpected error occurred';
+      shouldShowError = true;
+    }
+ 
+    if (shouldShowError) {
+      new Notice(userMessage);
+    }
+ 
+    // Log detailed error for debugging
+    console.error(
+      `[OllamaPlugin${context ? ' ' + context : ''}] ${error instanceof Error ? error.message : 'Unknown error'}`
+    );
+    if (error instanceof Error) {
+      console.error('[Stack]', error.stack);
+    }
+  }
+ 
+  /**
+   * Get user-friendly message from specific error types
+   */
+  private static getUserFriendlyMessage(error: OllamaError): string {
+    switch (error.type) {
+      case ErrorType.NETWORK_ERROR:
+        if (error instanceof NetworkError) {
+          return 'Connection error. Please check if Ollama is running.';
+        }
+        return 'Network error. Please check your connection to Ollama.';
+ 
+      case ErrorType.API_ERROR:
+        Iif (error instanceof ApiError) {
+          return 'Ollama API error. Please check the Ollama logs for details.';
+        }
+        return 'API communication error. Please try again.';
+ 
+      case ErrorType.VALIDATION_ERROR:
+        if (error instanceof ValidationError) {
+          const details = error.validationDetails;
+          if (details?.field) {
+            return `Invalid ${details.field}. ${details.message || 'Please check your input.'}`;
+          }
+          return 'Input validation error. Please correct your input.';
+        }
+        return 'Input validation error. Please correct your input.';
+ 
+      case ErrorType.STREAMING_ERROR:
+        if (error instanceof StreamingError) {
+          return 'Response too long. Please try a shorter request.';
+        }
+        return 'Streaming error. Please try again.';
+ 
+      case ErrorType.TOOL_EXECUTION_ERROR:
+        if (error instanceof ToolExecutionError) {
+          return `Tool error: ${error.toolName || 'tool'} failed to execute. Please try again.`;
+        }
+        return 'Tool execution error. Please try a different command.';
+ 
+      case ErrorType.PATH_VALIDATION_ERROR:
+        if (error instanceof PathValidationError) {
+          return 'Invalid file path. Please use a relative path without special characters.';
+        }
+        return 'Path validation error. Please check your file path.';
+ 
+      case ErrorType.UNKNOWN_ERROR:
+        return 'An unexpected error occurred. Please try again.';
+ 
+      default:
+        return error.message || 'An error occurred';
+    }
+  }
+ 
+  /**
+   * Get user-friendly message from generic Error
+   */
+  /**
+   * Get user-friendly message from generic Error
+   * Note: This method uses substring matching which is inherently fragile.
+   * If an error message happens to contain certain keywords but isn't actually
+   * that type of error, it may be misclassified. This heuristic approach
+   * provides a good balance between robustness and accuracy for most common cases.
+   */
+  private static getUserFriendlyMessageFromError(error: Error): string {
+    const message = error.message.toLowerCase();
+ 
+    if (message.includes('timeout')) {
+      return 'Request timed out. Please check your Ollama connection.';
+    }
+ 
+    if (
+      message.includes('network') ||
+      message.includes('fetch') ||
+      message.includes('connection')
+    ) {
+      return 'Connection error. Please check if Ollama is running.';
+    }
+ 
+    if (message.includes('validation') || message.includes('format')) {
+      return 'Invalid input. Please check your message.';
+    }
+ 
+    if (message.includes('stream') || message.includes('chunk')) {
+      return 'Response too long. Please try a shorter request.';
+    }
+ 
+    if (message.includes('tool') || message.includes('function')) {
+      return 'Tool execution error. Please try a different command.';
+    }
+ 
+    if (message.includes('path') || message.includes('file')) {
+      return 'Invalid file path. Please use a relative path without special characters.';
+    }
+ 
+    return error.message;
+  }
+ 
+  /**
+   * Create specific error instances from different error types
+   */
+  static createNetworkError(message: string, statusCode?: number): NetworkError {
+    return new NetworkError(message, statusCode);
+  }
+ 
+  static createApiError(message: string, apiError?: any): ApiError {
+    return new ApiError(message, apiError);
+  }
+ 
+  static createValidationError(
+    message: string,
+    field?: string,
+    details?: Record<string, string>
+  ): ValidationError {
+    const validationDetails = field ? { field, message } : details;
+    return new ValidationError(message, validationDetails);
+  }
+ 
+  static createStreamingError(message: string, chunkDetails?: any): StreamingError {
+    return new StreamingError(message, chunkDetails);
+  }
+ 
+  static createToolExecutionError(message: string, toolName?: string): ToolExecutionError {
+    return new ToolExecutionError(message, toolName);
+  }
+ 
+  static createPathValidationError(message: string, invalidPath?: string): PathValidationError {
+    return new PathValidationError(message, invalidPath);
+  }
+ 
+  static createUnknownError(message: string): OllamaError {
+    return new OllamaError(message, ErrorType.UNKNOWN_ERROR);
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 99.16% + Statements + 119/120 +
+ + +
+ 91.66% + Branches + 33/36 +
+ + +
+ 100% + Functions + 16/16 +
+ + +
+ 99.09% + Lines + 110/111 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
vault-indexer.ts +
+
99.16%119/12091.66%33/36100%16/1699.09%110/111
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/ollama-client.ts.html b/coverage/lcov-report/ollama-client.ts.html new file mode 100644 index 0000000..2047365 --- /dev/null +++ b/coverage/lcov-report/ollama-client.ts.html @@ -0,0 +1,706 @@ + + + + + + Code coverage report for ollama-client.ts + + + + + + + + + +
+
+

All files ollama-client.ts

+
+ +
+ 87.65% + Statements + 71/81 +
+ + +
+ 82.05% + Branches + 32/39 +
+ + +
+ 71.42% + Functions + 5/7 +
+ + +
+ 88.46% + Lines + 69/78 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +28x +  +  +28x +  +  +28x +28x +28x +  +  +  +  +  +  +  +10x +  +10x +  +  +  +  +10x +10x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +10x +1x +  +  +9x +1x +  +  +  +8x +8x +1x +  +  +7x +  +7x +7x +  +7x +7x +7x +7x +7x +7x +7x +7x +12x +12x +7x +  +  +7x +7x +7x +7x +61x +61x +61x +9x +  +9x +1x +  +8x +  +  +  +  +  +  +  +  +53x +  +1x +1x +1x +  +  +  +52x +52x +  +  +  +52x +1x +  +  +  +  +51x +  +  +  +  +2x +2x +  +  +2x +  +7x +7x +  +  +  +  +  +  +  +  +  +  +5x +5x +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +5x +1x +  +  +4x +  +4x +4x +4x +  +  +  +  +  +  +21x +3x +3x +  +  +  + 
import { OllamaMessage, OllamaTool, ToolCall } from './types';
+ 
+interface FetchResponse {
+  ok: boolean;
+  status: number;
+  headers?: {
+    get: (name: string) => string | null;
+  };
+  body?: {
+    getReader: () => ReadableStreamDefaultReader<Uint8Array>;
+  } | null;
+  json?: () => Promise<any>;
+}
+ 
+interface FetchOptions {
+  method: string;
+  headers: Record<string, string>;
+  body: string;
+  signal?: AbortSignal;
+}
+ 
+export class OllamaClient {
+  private url: string;
+  private model: string;
+  private abortController: AbortController | null = null;
+ 
+  // Mock fetch function for testing
+  private fetchFn: typeof fetch = fetch;
+ 
+  constructor(url: string, model: string, fetchFn?: typeof fetch) {
+    this.url = url;
+    this.model = model;
+    if (fetchFn) this.fetchFn = fetchFn;
+  }
+ 
+  async streamChat(
+    messages: OllamaMessage[],
+    tools: OllamaTool[],
+    timeoutMs: number = 60000
+  ): Promise<AsyncIterable<{ content: string; tool_calls?: ToolCall[] }>> {
+    this.abortController = new AbortController();
+ 
+    const timeoutId = setTimeout(() => {
+      this.abortController?.abort();
+    }, timeoutMs);
+ 
+    let response: FetchResponse;
+    try {
+      response = await this.fetchFn(`${this.url}/api/chat`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          model: this.model,
+          messages,
+          tools,
+          stream: true,
+        }),
+        signal: this.abortController.signal,
+      } as FetchOptions);
+    } catch (fetchError: any) {
+      clearTimeout(timeoutId);
+      this.abortController = null;
+      Iif (fetchError.name === 'AbortError' || fetchError.code === 'ABORT_ERR') {
+        throw new Error('Request timeout while connecting to Ollama');
+      }
+      throw fetchError;
+    }
+ 
+    if (!response.ok) {
+      throw new Error(`Ollama API error: ${response.status}`);
+    }
+ 
+    if (!response.body) {
+      throw new Error('No response body');
+    }
+ 
+    // Validate response structure
+    const contentType = response.headers?.get('content-type');
+    if (!contentType?.match(/application\/(x-ndjson|json)/)) {
+      throw new Error('Invalid response format');
+    }
+ 
+    const reader = response.body.getReader();
+ 
+    const self = this;
+    return {
+      [Symbol.asyncIterator]: async function* () {
+        const decoder = new TextDecoder();
+        let buffer = '';
+        let chunkCount = 0;
+        let skippedChunks = 0;
+        const maxChunks = 1000; // Safety limit
+        const maxSkipped = 50; // Fail if too many chunks are malformed
+        try {
+          while (true) {
+            const { done, value } = await reader.read();
+            if (done) break;
+            Iif (++chunkCount > maxChunks) {
+              throw new Error('Response too long, stopped streaming');
+            }
+            buffer += decoder.decode(value, { stream: true });
+            const lines = buffer.split('\n');
+            buffer = lines.pop() || '';
+            for (const line of lines) {
+              Iif (line.trim() === '') continue;
+              try {
+                const data = JSON.parse(line);
+                if (data.message && typeof data.message === 'object') {
+                  // Validate message structure
+                  if (data.message.error && typeof data.message.error === 'string') {
+                    throw new Error(`Ollama error: ${data.message.error}`);
+                  }
+                  yield {
+                    content: data.message.content || '',
+                    tool_calls: Array.isArray(data.message.tool_calls)
+                      ? data.message.tool_calls
+                      : [],
+                  };
+                }
+              } catch (parseError) {
+                // Check if this is an Ollama error (thrown intentionally) vs a parse error
+                if (parseError instanceof Error && parseError.message.startsWith('Ollama error:')) {
+                  // This is an intentional Ollama error, re-throw it
+                  self.abortController = null;
+                  reader.releaseLock();
+                  throw parseError;
+                }
+ 
+                // This is a parse error, skip the malformed chunk
+                skippedChunks++;
+                console.warn(
+                  `[OllamaClient] Skipped malformed chunk ${skippedChunks}/${maxSkipped}:`,
+                  parseError instanceof Error ? parseError.message : String(parseError)
+                );
+                if (skippedChunks > maxSkipped) {
+                  throw new Error(
+                    `Too many malformed response chunks (${skippedChunks}). Connection may be degraded.`
+                  );
+                }
+                // Skip invalid chunks but continue streaming
+                continue;
+              }
+            }
+          }
+        } catch (streamError) {
+          self.abortController = null;
+          Iif (streamError instanceof Error && streamError.name === 'AbortError') {
+            throw new Error('Streaming request was cancelled');
+          }
+          throw streamError;
+        } finally {
+          reader.releaseLock();
+          self.abortController = null;
+        }
+      },
+    };
+  }
+ 
+  async chat(
+    messages: OllamaMessage[],
+    tools: OllamaTool[],
+    timeoutMs: number = 30000
+  ): Promise<{ content: string; tool_calls?: ToolCall[] }> {
+    const abortController = new AbortController();
+    const timeoutId = setTimeout(() => {
+      abortController.abort();
+    }, timeoutMs);
+ 
+    const response = await this.fetchFn(`${this.url}/api/chat`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        model: this.model,
+        messages,
+        tools,
+        stream: false,
+      }),
+      signal: abortController.signal,
+    } as FetchOptions);
+ 
+    clearTimeout(timeoutId);
+ 
+    if (!response.ok) {
+      throw new Error(`Ollama API error: ${response.status}`);
+    }
+ 
+    const responseData = await response.json();
+ 
+    const data = responseData;
+    const messageData = data.message;
+    return {
+      content: messageData?.content || '',
+      tool_calls: messageData?.tool_calls || [],
+    };
+  }
+ 
+  cancelStream(): void {
+    if (this.abortController) {
+      this.abortController.abort();
+      this.abortController = null;
+    }
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/tool-executor.ts.html b/coverage/lcov-report/tool-executor.ts.html new file mode 100644 index 0000000..da36feb --- /dev/null +++ b/coverage/lcov-report/tool-executor.ts.html @@ -0,0 +1,265 @@ + + + + + + Code coverage report for tool-executor.ts + + + + + + + + + +
+
+

All files tool-executor.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 83.33% + Branches + 10/12 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 24/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61  +2x +2x +  +2x +  +  +  +  +38x +38x +  +  +  +  +  +28x +  +28x +  +  +27x +  +27x +26x +26x +  +1x +  +  +  +  +  +  +26x +2x +  +  +  +24x +2x +  +  +22x +1x +  +  +21x +21x +14x +  +  +7x +6x +  +  +1x +  +  +  + 
import { Vault, TFile, Notice, App } from 'obsidian';
+import { ToolCall, ToolResult, ToolExecutionError, PathValidationError } from './types';
+import { validatePath } from './utils';
+ 
+export class ToolExecutor {
+  private vault: Vault;
+  private app: App;
+ 
+  constructor(vault: Vault, app: App) {
+    this.vault = vault;
+    this.app = app;
+  }
+ 
+  async handleToolCall(call: ToolCall): Promise<ToolResult> {
+    const {
+      function: { name, arguments: args },
+    } = call;
+ 
+    switch (name) {
+      case 'create_file': {
+        let filePath: string, content: string;
+        try {
+          // Handle both string (JSON) and object arguments, since some Ollama versions return args as an object
+          const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
+          filePath = parsedArgs.path;
+          content = parsedArgs.content;
+        } catch (e) {
+          throw new ToolExecutionError(
+            `Invalid arguments provided for create_file: ${e instanceof Error ? e.message : 'Unknown parsing error'}`,
+            'create_file'
+          );
+        }
+ 
+        // Validate content is a string
+        if (typeof content !== 'string') {
+          throw new ToolExecutionError('Content must be a string', 'create_file');
+        }
+ 
+        // Validate path using shared utility
+        if (typeof filePath !== 'string') {
+          throw new ToolExecutionError('Path must be a string', 'create_file');
+        }
+ 
+        if (!filePath) {
+          throw new ToolExecutionError('Path is required', 'create_file');
+        }
+ 
+        const pathValidation = validatePath(filePath);
+        if (!pathValidation.valid) {
+          throw new PathValidationError(pathValidation.error || 'Path validation failed', filePath);
+        }
+ 
+        await this.vault.create(filePath, content);
+        return { success: true, message: 'File created successfully' };
+      }
+      default:
+        return { success: false, message: `Unknown tool: ${name}` };
+    }
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/types.ts.html b/coverage/lcov-report/types.ts.html new file mode 100644 index 0000000..a9d8674 --- /dev/null +++ b/coverage/lcov-report/types.ts.html @@ -0,0 +1,487 @@ + + + + + + Code coverage report for types.ts + + + + + + + + + +
+
+

All files types.ts

+
+ +
+ 100% + Statements + 37/37 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 37/37 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135  +  +  +  +  +  +3x +3x +3x +3x +3x +3x +3x +3x +  +  +3x +  +  +35x +35x +  +35x +35x +  +  +  +3x +  +  +4x +  +4x +4x +  +  +  +3x +  +  +1x +  +1x +1x +  +  +  +3x +  +  +3x +  +3x +3x +  +  +  +3x +  +  +2x +  +2x +2x +  +  +  +3x +  +  +8x +  +8x +8x +  +  +  +3x +  +  +16x +  +16x +16x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
export interface PluginSettings {
+  ollamaUrl: string;
+  model: string;
+  lastIndexTime: number;
+}
+ 
+export enum ErrorType {
+  NETWORK_ERROR = 'network_error',
+  API_ERROR = 'api_error',
+  VALIDATION_ERROR = 'validation_error',
+  STREAMING_ERROR = 'streaming_error',
+  TOOL_EXECUTION_ERROR = 'tool_execution_error',
+  PATH_VALIDATION_ERROR = 'path_validation_error',
+  UNKNOWN_ERROR = 'unknown_error',
+}
+ 
+export class OllamaError extends Error {
+  constructor(
+    message: string,
+    public readonly type: ErrorType,
+    public readonly details?: Record<string, any>
+  ) {
+    super(message);
+    this.name = 'OllamaError';
+  }
+}
+ 
+export class NetworkError extends OllamaError {
+  constructor(
+    message: string,
+    public readonly statusCode?: number
+  ) {
+    super(message, ErrorType.NETWORK_ERROR, { statusCode });
+    this.name = 'NetworkError';
+  }
+}
+ 
+export class ApiError extends OllamaError {
+  constructor(
+    message: string,
+    public readonly apiError?: any
+  ) {
+    super(message, ErrorType.API_ERROR, { apiError });
+    this.name = 'ApiError';
+  }
+}
+ 
+export class ValidationError extends OllamaError {
+  constructor(
+    message: string,
+    public readonly validationDetails?: Record<string, string>
+  ) {
+    super(message, ErrorType.VALIDATION_ERROR, validationDetails);
+    this.name = 'ValidationError';
+  }
+}
+ 
+export class StreamingError extends OllamaError {
+  constructor(
+    message: string,
+    public readonly chunkDetails?: any
+  ) {
+    super(message, ErrorType.STREAMING_ERROR, chunkDetails);
+    this.name = 'StreamingError';
+  }
+}
+ 
+export class ToolExecutionError extends OllamaError {
+  constructor(
+    message: string,
+    public readonly toolName?: string
+  ) {
+    super(message, ErrorType.TOOL_EXECUTION_ERROR, { toolName });
+    this.name = 'ToolExecutionError';
+  }
+}
+ 
+export class PathValidationError extends OllamaError {
+  constructor(
+    message: string,
+    public readonly invalidPath?: string
+  ) {
+    super(message, ErrorType.PATH_VALIDATION_ERROR, { invalidPath });
+    this.name = 'PathValidationError';
+  }
+}
+ 
+export interface OllamaMessage {
+  role: 'system' | 'user' | 'assistant' | 'tool';
+  content: string;
+  tool_calls?: ToolCall[];
+}
+ 
+export interface ToolCall {
+  function: {
+    name: string;
+    arguments: string | Record<string, any>;
+  };
+}
+ 
+export interface OllamaTool {
+  type: 'function';
+  function: {
+    name: string;
+    description: string;
+    parameters: {
+      type: 'object';
+      properties: Record<string, { type: string }>;
+      required: string[];
+    };
+  };
+}
+ 
+export interface ToolResult {
+  success: boolean;
+  message: string;
+  // Adding optional details field for better error reporting
+  details?: Record<string, any>;
+}
+ 
+export interface VaultIndexEntry {
+  title: string;
+  content: string;
+  score: number;
+}
+ 
+export interface ChatMessage {
+  id: string;
+  role: 'user' | 'assistant' | 'tool';
+  content: string;
+  timestamp: number;
+  isStreaming?: boolean;
+  tool_calls?: ToolCall[];
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/utils.ts.html b/coverage/lcov-report/utils.ts.html new file mode 100644 index 0000000..e49da1f --- /dev/null +++ b/coverage/lcov-report/utils.ts.html @@ -0,0 +1,241 @@ + + + + + + Code coverage report for utils.ts + + + + + + + + + +
+
+

All files utils.ts

+
+ +
+ 95.23% + Statements + 20/21 +
+ + +
+ 90% + Branches + 9/10 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 95.23% + Lines + 20/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53  +  +  +  +2x +  +21x +  +  +21x +  +  +  +21x +  +  +  +  +  +2x +21x +  +  +21x +21x +2x +  +  +  +19x +2x +  +  +  +17x +1x +  +  +  +16x +16x +8x +  +  +  +8x +8x +1x +  +  +7x +  + 
/**
+ * Normalizes file paths for browser/ Obsidian environment
+ * Replaces multiple slashes with single slash and handles forward/backward slashes
+ */
+export function normalizePath(path: string): string {
+  // Replace multiple slashes with single slash
+  let normalized = path.replace(/[\\\/]+/g, '/');
+ 
+  // Remove trailing slash unless it's the root
+  Iif (normalized.length > 1 && normalized.endsWith('/')) {
+    normalized = normalized.slice(0, -1);
+  }
+ 
+  return normalized;
+}
+ 
+/**
+ * Validates a path string for safety (no traversal, no absolute paths, no invalid chars)
+ */
+export function validatePath(path: string): { valid: boolean; error?: string } {
+  const normalized = normalizePath(path);
+ 
+  // Check for path traversal by looking for .. as a path segment (not just substring in filenames)
+  const segments = normalized.split('/');
+  if (segments.includes('..')) {
+    return { valid: false, error: 'Path traversal not allowed' };
+  }
+ 
+  // Check if absolute path
+  if (normalized.startsWith('/') || normalized.startsWith('\\')) {
+    return { valid: false, error: 'Absolute paths not allowed' };
+  }
+ 
+  // Check for windows drive letters
+  if (/^[a-zA-Z]:/.test(normalized)) {
+    return { valid: false, error: 'Absolute paths not allowed' };
+  }
+ 
+  // Check for invalid characters
+  const invalidChars = /[\<\>\:\"\|\\\?\*~]/;
+  if (invalidChars.test(path)) {
+    return { valid: false, error: 'Path contains illegal characters' };
+  }
+ 
+  // Check path length
+  const MAX_PATH_LENGTH = 200;
+  if (path.length > MAX_PATH_LENGTH) {
+    return { valid: false, error: 'Path too long' };
+  }
+ 
+  return { valid: true };
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/vault-indexer.ts.html b/coverage/lcov-report/vault-indexer.ts.html new file mode 100644 index 0000000..ebdf84a --- /dev/null +++ b/coverage/lcov-report/vault-indexer.ts.html @@ -0,0 +1,1276 @@ + + + + + + Code coverage report for vault-indexer.ts + + + + + + + + + +
+
+

All files vault-indexer.ts

+
+ +
+ 99.16% + Statements + 119/120 +
+ + +
+ 91.66% + Branches + 33/36 +
+ + +
+ 100% + Functions + 16/16 +
+ + +
+ 99.09% + Lines + 110/111 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398  +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +31x +  +  +  +16x +16x +  +16x +16x +2x +  +  +  +14x +15x +15x +  +54x +53x +53x +  +53x +50x +  +  +  +  +  +3x +  +  +  +15x +54x +50x +4x +1x +  +  +  +  +  +  +38x +  +  +  +  +  +  +268x +  +  +  +  +  +841x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +841x +  +  +  +  +  +  +  +60x +60x +  +  +60x +60x +  +60x +  +60x +3x +  +  +  +60x +60x +60x +60x +2x +2x +2x +3x +3x +3x +3x +3x +  +  +  +  +  +60x +  +  +60x +60x +60x +60x +63x +  +63x +3x +  +  +60x +2x +  +58x +58x +1x +  +  +60x +  +  +60x +60x +  +60x +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +58x +58x +58x +58x +  +  +58x +50x +  +  +58x +64x +  +  +64x +1x +  +  +  +64x +1x +  +  +  +64x +1x +1x +1x +  +  +  +  +123x +57x +  +  +  +295x +64x +  +58x +  +  +  +64x +58x +  +  +64x +  +  +  +  +58x +58x +  +  +  +58x +  +  +  +  +  +  +  +  +489x +220x +  +  +  +269x +1x +  +268x +1x +  +  +  +267x +534x +520x +519x +  +  +267x +  +  + 
import { Vault, TFile } from 'obsidian';
+import { VaultIndexEntry } from './types';
+ 
+const MAX_CONTENT_PREVIEW_LENGTH = 500;
+const BATCH_SIZE = 10;
+const MAX_TOKENS = 10000;
+const TITLE_WEIGHT = 10;
+const HEADING_WEIGHT = 5;
+const FRONTMATTER_WEIGHT = 8;
+const FIRST_PARAGRAPH_WEIGHT = 3;
+const BODY_WEIGHT = 1;
+const PHRASE_MATCH_BONUS = 2;
+const EXACT_WORD_MATCH_BONUS = 1.5;
+ 
+interface TokenizedContent {
+  text: string;
+  tokens: string[];
+  title: string;
+  titleTokens: string[];
+  headings: string[];
+  headingTokens: string[][];
+  frontmatter: Record<string, string>;
+  frontmatterTokens: string[];
+  firstParagraph: string;
+  firstParagraphTokens: string[];
+}
+ 
+export class VaultIndexer {
+  private vault: Vault;
+ 
+  constructor(vault: Vault) {
+    this.vault = vault;
+  }
+ 
+  async searchVault(query: string, limit: number = 5): Promise<VaultIndexEntry[]> {
+    const files = this.vault.getMarkdownFiles();
+    const results: VaultIndexEntry[] = [];
+ 
+    const queryTokens = this.tokenize(query);
+    if (queryTokens.length === 0) {
+      return [];
+    }
+ 
+    // Process files in batches with concurrency limit
+    for (let i = 0; i < files.length; i += BATCH_SIZE) {
+      const batch = files.slice(i, i + BATCH_SIZE);
+      const batchResults = await Promise.allSettled(
+        batch.map(async (file) => {
+          const fullContent = await this.vault.read(file);
+          const tokenized = this.tokenizeContent(fullContent, file);
+          const score = this.calculateWeightedScore(tokenized, query, queryTokens);
+ 
+          if (score > 0) {
+            return {
+              title: file.basename,
+              content: fullContent.substring(0, MAX_CONTENT_PREVIEW_LENGTH),
+              score,
+            } as VaultIndexEntry;
+          }
+          return null;
+        })
+      );
+ 
+      for (const result of batchResults) {
+        if (result.status === 'fulfilled' && result.value !== null) {
+          results.push(result.value);
+        } else if (result.status === 'rejected') {
+          console.warn(
+            `[VaultIndexer] Failed to read file: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
+          );
+        }
+      }
+    }
+ 
+    return results.sort((a, b) => b.score - a.score).slice(0, limit);
+  }
+ 
+  /**
+   * Tokenize a string into lowercase words, filtering out stop words and very short tokens
+   */
+  private tokenize(text: string): string[] {
+    return text
+      .toLowerCase()
+      .replace(/[^\w\s]/g, ' ')
+      .split(/\s+/)
+      .filter((token) => {
+        // Filter out common stop words and very short tokens
+        const stopWords = new Set([
+          'a',
+          'an',
+          'the',
+          'and',
+          'or',
+          'but',
+          'in',
+          'on',
+          'at',
+          'to',
+          'for',
+          'of',
+          'with',
+          'by',
+          'is',
+          'are',
+          'was',
+          'were',
+          'be',
+          'been',
+          'have',
+          'has',
+          'had',
+          'do',
+          'does',
+          'did',
+          'will',
+          'would',
+          'could',
+          'should',
+          'may',
+          'might',
+          'must',
+          'shall',
+          'can',
+          'need',
+          'dare',
+          'ought',
+          'used',
+          'it',
+          'its',
+          'this',
+          'that',
+          'these',
+          'those',
+          'i',
+          'you',
+          'he',
+          'she',
+          'we',
+          'they',
+          'me',
+          'him',
+          'her',
+          'us',
+          'them',
+          'my',
+          'your',
+          'his',
+          'our',
+          'their',
+          'mine',
+          'yours',
+          'hers',
+          'ours',
+          'theirs',
+          'what',
+          'which',
+          'who',
+          'whom',
+          'whose',
+          'where',
+          'when',
+          'why',
+          'how',
+          'not',
+          'no',
+          'nor',
+          'so',
+          'if',
+          'then',
+          'than',
+          'too',
+          'very',
+          'just',
+          'about',
+          'above',
+          'after',
+          'again',
+          'all',
+          'am',
+          'any',
+          'as',
+          'because',
+          'before',
+          'being',
+          'below',
+          'between',
+          'both',
+          'during',
+          'each',
+          'few',
+          'further',
+          'get',
+          'got',
+          'here',
+          'into',
+          'more',
+          'most',
+          'much',
+          'myself',
+          'nothing',
+          'only',
+          'other',
+          'out',
+          'over',
+          'own',
+          'same',
+          'some',
+          'such',
+          'there',
+          'through',
+          'under',
+          'until',
+          'up',
+          'while',
+          'why',
+          'yes',
+          'also',
+          'from',
+        ]);
+        return token.length > 1 && !stopWords.has(token);
+      });
+  }
+ 
+  /**
+   * Tokenize markdown content into structured components
+   */
+  private tokenizeContent(content: string, file: TFile): TokenizedContent {
+    const title = file.basename;
+    const titleTokens = this.tokenize(title);
+ 
+    // Extract headings
+    const headingRegex = /^(#{1,6})\s+(.+)$/gm;
+    const headings: string[] = [];
+    let match: RegExpExecArray | null;
+    const headingRegexState = /^(#{1,6})\s+(.+)$/gm;
+ 
+    while ((match = headingRegexState.exec(content)) !== null) {
+      headings.push(match[2]);
+    }
+ 
+    // Extract frontmatter (YAML between --- markers)
+    const frontmatter: Record<string, string> = {};
+    const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
+    const frontmatterMatch = frontmatterRegex.exec(content);
+    if (frontmatterMatch) {
+      const frontmatterContent = frontmatterMatch[1];
+      const lines = frontmatterContent.split('\n');
+      for (const line of lines) {
+        const colonIndex = line.indexOf(':');
+        if (colonIndex > 0) {
+          const key = line.substring(0, colonIndex).trim();
+          const value = line.substring(colonIndex + 1).trim();
+          frontmatter[key] = value;
+        }
+      }
+    }
+ 
+    // Get frontmatter tokens from values
+    const frontmatterTokens = this.tokenize(Object.values(frontmatter).join(' '));
+ 
+    // Extract first paragraph (non-empty lines after frontmatter, stop at paragraph break)
+    const cleanContent = content.replace(/^---\n[\s\S]*?\n---/, '').trim();
+    const lines = cleanContent.split('\n');
+    let firstParagraph = '';
+    for (const line of lines) {
+      const trimmed = line.trim();
+      // Stop at empty line (paragraph break)
+      if (!trimmed) {
+        break;
+      }
+      // Skip headings
+      if (trimmed.startsWith('#')) {
+        continue;
+      }
+      firstParagraph += trimmed + ' ';
+      if (firstParagraph.length > 200) {
+        break;
+      }
+    }
+    const firstParagraphTokens = this.tokenize(firstParagraph);
+ 
+    // Get body tokens (limit to prevent memory issues with huge files)
+    const bodyText = cleanContent.substring(0, MAX_TOKENS);
+    const tokens = this.tokenize(bodyText);
+ 
+    return {
+      text: bodyText,
+      tokens,
+      title,
+      titleTokens,
+      headings,
+      headingTokens: headings.map((h) => this.tokenize(h)),
+      frontmatter,
+      frontmatterTokens,
+      firstParagraph,
+      firstParagraphTokens,
+    };
+  }
+ 
+  /**
+   * Calculate a weighted score based on where query tokens appear
+   * Uses a combination of position weighting, exact matching, and phrase matching
+   */
+  private calculateWeightedScore(
+    tokenized: TokenizedContent,
+    query: string,
+    queryTokens: string[]
+  ): number {
+    let score = 0;
+    const queryLower = query.toLowerCase();
+    const contentLower = tokenized.text.toLowerCase();
+    const contentWithBoundaries = '\\b' + contentLower + '\\b';
+ 
+    // Check for exact phrase match (bonus)
+    if (queryLower.length > 0 && contentLower.includes(queryLower)) {
+      score += PHRASE_MATCH_BONUS * queryTokens.length;
+    }
+ 
+    for (const queryToken of queryTokens) {
+      let tokenScore = 0;
+ 
+      // Title match (highest priority)
+      if (tokenized.titleTokens.some((t) => this.exactMatch(t, queryToken))) {
+        tokenScore += TITLE_WEIGHT;
+      }
+ 
+      // Frontmatter match (high priority - often contains tags/categories)
+      if (tokenized.frontmatterTokens.some((t) => this.exactMatch(t, queryToken))) {
+        tokenScore += FRONTMATTER_WEIGHT;
+      }
+ 
+      // Heading match (high priority)
+      for (const headingTokens of tokenized.headingTokens) {
+        if (headingTokens.some((t) => this.exactMatch(t, queryToken))) {
+          tokenScore += HEADING_WEIGHT;
+          break; // Only count once per query token
+        }
+      }
+ 
+      // First paragraph match (medium priority - likely contains topic summary)
+      if (tokenized.firstParagraphTokens.some((t) => this.exactMatch(t, queryToken))) {
+        tokenScore += FIRST_PARAGRAPH_WEIGHT;
+      }
+ 
+      // Body match (lowest priority)
+      const bodyMatchCount = tokenized.tokens.filter((t) => this.exactMatch(t, queryToken)).length;
+      if (bodyMatchCount > 0) {
+        // Use logarithmic scaling to prevent very frequent words from dominating
+        tokenScore += BODY_WEIGHT * Math.log(1 + bodyMatchCount);
+      }
+ 
+      // Exact word boundary bonus
+      if (new RegExp(`\\b${queryToken}\\b`).test(contentLower)) {
+        tokenScore *= EXACT_WORD_MATCH_BONUS;
+      }
+ 
+      score += tokenScore;
+    }
+ 
+    // Normalize by document length to prevent bias toward longer documents
+    // Use a gentle normalization: divide by log of token count + 1
+    const lengthNorm = Math.log(1 + tokenized.tokens.length / 100);
+    Iif (lengthNorm > 1) {
+      score /= lengthNorm;
+    }
+ 
+    return score;
+  }
+ 
+  /**
+   * Check for exact or stemmed word match
+   * Handles plurals and common suffixes
+   */
+  private exactMatch(textToken: string, queryToken: string): boolean {
+    // Exact match
+    if (textToken === queryToken) {
+      return true;
+    }
+ 
+    // Handle plurals
+    if (queryToken.endsWith('s') && textToken === queryToken.slice(0, -1)) {
+      return true;
+    }
+    if (textToken.endsWith('s') && textToken.slice(0, -1) === queryToken) {
+      return true;
+    }
+ 
+    // Handle -ed and -ing suffixes (simple stemmer)
+    const stem = (word: string): string => {
+      if (word.endsWith('ing')) return word.slice(0, -3);
+      if (word.endsWith('ed')) return word.slice(0, -2);
+      return word;
+    };
+ 
+    return stem(textToken) === stem(queryToken);
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..003a1d9 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,188 @@ +TN: +SF:src/vault-indexer.ts +FN:31,(anonymous_0) +FN:35,(anonymous_1) +FN:48,(anonymous_2) +FN:75,(anonymous_3) +FN:81,(anonymous_4) +FN:86,(anonymous_5) +FN:227,(anonymous_6) +FN:292,(anonymous_7) +FN:304,(anonymous_8) +FN:323,(anonymous_9) +FN:328,(anonymous_10) +FN:334,(anonymous_11) +FN:341,(anonymous_12) +FN:346,(anonymous_13) +FN:374,(anonymous_14) +FN:389,(anonymous_15) +FNF:16 +FNH:16 +FNDA:31,(anonymous_0) +FNDA:16,(anonymous_1) +FNDA:54,(anonymous_2) +FNDA:38,(anonymous_3) +FNDA:268,(anonymous_4) +FNDA:841,(anonymous_5) +FNDA:60,(anonymous_6) +FNDA:3,(anonymous_7) +FNDA:58,(anonymous_8) +FNDA:63,(anonymous_9) +FNDA:1,(anonymous_10) +FNDA:1,(anonymous_11) +FNDA:123,(anonymous_12) +FNDA:295,(anonymous_13) +FNDA:489,(anonymous_14) +FNDA:534,(anonymous_15) +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +DA:8,1 +DA:9,1 +DA:10,1 +DA:11,1 +DA:12,1 +DA:13,1 +DA:28,1 +DA:32,31 +DA:36,16 +DA:37,16 +DA:39,16 +DA:40,16 +DA:41,2 +DA:45,14 +DA:46,15 +DA:47,15 +DA:49,54 +DA:50,53 +DA:51,53 +DA:53,53 +DA:54,50 +DA:60,3 +DA:64,15 +DA:65,54 +DA:66,50 +DA:67,4 +DA:68,1 +DA:75,38 +DA:82,268 +DA:88,841 +DA:220,841 +DA:228,60 +DA:229,60 +DA:232,60 +DA:233,60 +DA:235,60 +DA:237,60 +DA:238,3 +DA:242,60 +DA:243,60 +DA:244,60 +DA:245,60 +DA:246,2 +DA:247,2 +DA:248,2 +DA:249,3 +DA:250,3 +DA:251,3 +DA:252,3 +DA:253,3 +DA:259,60 +DA:262,60 +DA:263,60 +DA:264,60 +DA:265,60 +DA:266,63 +DA:268,63 +DA:269,3 +DA:272,60 +DA:273,2 +DA:275,58 +DA:276,58 +DA:277,1 +DA:280,60 +DA:283,60 +DA:284,60 +DA:286,60 +DA:292,3 +DA:309,58 +DA:310,58 +DA:311,58 +DA:312,58 +DA:315,58 +DA:316,50 +DA:319,58 +DA:320,64 +DA:323,64 +DA:324,1 +DA:328,64 +DA:329,1 +DA:333,64 +DA:334,1 +DA:335,1 +DA:336,1 +DA:341,123 +DA:342,57 +DA:346,295 +DA:347,64 +DA:349,58 +DA:353,64 +DA:354,58 +DA:357,64 +DA:362,58 +DA:363,58 +DA:364,0 +DA:367,58 +DA:376,489 +DA:377,220 +DA:381,269 +DA:382,1 +DA:384,268 +DA:385,1 +DA:389,267 +DA:390,534 +DA:391,520 +DA:392,519 +DA:395,267 +LF:111 +LH:110 +BRDA:35,0,0,0 +BRDA:40,1,0,2 +BRDA:53,2,0,50 +BRDA:65,3,0,50 +BRDA:65,3,1,4 +BRDA:65,4,0,54 +BRDA:65,4,1,53 +BRDA:67,5,0,1 +BRDA:69,6,0,1 +BRDA:69,6,1,0 +BRDA:220,7,0,841 +BRDA:220,7,1,710 +BRDA:245,8,0,2 +BRDA:250,9,0,3 +BRDA:268,10,0,3 +BRDA:272,11,0,2 +BRDA:276,12,0,1 +BRDA:315,13,0,50 +BRDA:315,14,0,58 +BRDA:315,14,1,57 +BRDA:323,15,0,1 +BRDA:328,16,0,1 +BRDA:334,17,0,1 +BRDA:341,18,0,57 +BRDA:347,19,0,58 +BRDA:353,20,0,58 +BRDA:363,21,0,0 +BRDA:376,22,0,220 +BRDA:381,23,0,1 +BRDA:381,24,0,269 +BRDA:381,24,1,1 +BRDA:384,25,0,1 +BRDA:384,26,0,268 +BRDA:384,26,1,24 +BRDA:390,27,0,14 +BRDA:391,28,0,1 +BRF:36 +BRH:33 +end_of_record diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..71b9632 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + testMatch: ['**/tests/**/*.test.ts'], + moduleFileExtensions: ['ts', 'js', 'json', 'node'], + transform: { + '^.+\\.ts$': 'ts-jest', + }, + coverageDirectory: 'coverage', + collectCoverage: true, + coverageReporters: ['text', 'lcov'], + setupFiles: ['./jest.setup.js'], + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.test.json', + }, + }, +}; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..e69de29 diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..e1abac7 --- /dev/null +++ b/main.ts @@ -0,0 +1,77 @@ +import { Plugin, WorkspaceLeaf, App, Setting, Notice, PluginSettingTab } from 'obsidian'; +import { ChatView } from './src/chat-view'; +import { PluginSettings } from './src/types'; + +export default class OllamaPlugin extends Plugin { + settings: PluginSettings = { + ollamaUrl: 'http://localhost:11434', + model: 'llama3', + lastIndexTime: 0, + }; + + async onload() { + await this.loadSettings(); + + this.registerView( + 'ollama-chat-view', + (leaf: WorkspaceLeaf) => new ChatView(leaf, this.settings) + ); + + this.addRibbonIcon('message-square', 'Ollama Chat', async () => { + const leaf = this.app.workspace.getLeaf(); + await leaf.setViewState({ + type: 'ollama-chat-view', + active: true, + }); + this.app.workspace.revealLeaf(leaf); + }); + + this.addSettingTab(new OllamaSettingTab(this.app, this)); + } + + async loadSettings() { + this.settings = Object.assign({}, this.settings, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } +} + +class OllamaSettingTab extends PluginSettingTab { + private plugin: OllamaPlugin; + + constructor(app: App, plugin: OllamaPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const container = this.containerEl.createDiv() as HTMLElement; + container.empty(); + + new Setting(container) + .setName('Ollama URL') + .setDesc('URL of your Ollama instance') + .addText((text) => + text.setValue(this.plugin.settings.ollamaUrl).onChange(async (value) => { + this.plugin.settings.ollamaUrl = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(container) + .setName('Model') + .setDesc('Model to use for chat') + .addText((text) => + text.setValue(this.plugin.settings.model).onChange(async (value) => { + this.plugin.settings.model = value; + await this.plugin.saveSettings(); + }) + ); + } + + hide(): void { + this.containerEl.empty(); + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..909c136 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6274 @@ +{ + "name": "ollama-plugin", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ollama-plugin", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "jest-environment-jsdom": "^30.3.0", + "node-fetch": "^3.3.2", + "obsidian": "^1.4.11" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "^20.11.19", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "prettier": "^3.2.5", + "ts-jest": "^29.1.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "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==", + "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.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "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", + "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/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.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-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-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-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-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-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-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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "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-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.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-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "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-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "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/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/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz", + "integrity": "sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==", + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.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/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/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT", + "peer": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT", + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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/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": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.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-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", + "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "license": "MIT" + }, + "node_modules/obsidian": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", + "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "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-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/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-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT", + "peer": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT", + "peer": true + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "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/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8edf96e --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "ollama-plugin", + "version": "1.0.0", + "description": "Ollama integration plugin for Obsidian", + "main": "main.ts", + "scripts": { + "test": "jest", + "build": "tsc", + "watch": "tsc --watch", + "lint": "eslint . --ext .ts", + "format": "prettier --write ." + }, + "keywords": [ + "obsidian", + "ollama", + "chat", + "ai", + "plugin" + ], + "author": "Anonymous", + "license": "MIT", + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "^20.11.19", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "prettier": "^3.2.5", + "ts-jest": "^29.1.2", + "typescript": "^5.3.3" + }, + "dependencies": { + "jest-environment-jsdom": "^30.3.0", + "node-fetch": "^3.3.2", + "obsidian": "^1.4.11" + } +} diff --git a/rules b/rules new file mode 100644 index 0000000..fec9752 --- /dev/null +++ b/rules @@ -0,0 +1,111 @@ +# Code Review Agent Rules +**Models:** devstral-small-2:24b · gemma4:31b · qwen3:27b +**Role:** Senior Code Reviewer + +--- + +## Identity & Mindset + +You are a senior engineer conducting a thorough code review. Be direct, specific, and constructive. Every comment must reference the exact file and line. Never praise for the sake of it — only flag what genuinely matters. Prioritize correctness and maintainability over style. + +--- + +## Review Workflow + +1. **Understand intent** — Before reviewing, state in one sentence what the code is trying to do. +2. **Read fully first** — Scan all changed files before commenting on any single one. +3. **Categorize findings** — Label every issue with a severity (see below). +4. **Cite precisely** — Every finding must include: file path, line number(s), and a concrete suggestion. +5. **Summarize** — End with an overall verdict and a prioritized list of must-fix items. + +--- + +## Severity Labels + +Use exactly these labels — no others: + +| Label | Meaning | +|---|---| +| `[CRITICAL]` | Bug, security flaw, data loss risk — must fix before merge | +| `[MAJOR]` | Logic error, bad abstraction, serious performance issue | +| `[MINOR]` | Code smell, unnecessary complexity, poor naming | +| `[NIT]` | Style, formatting, trivial rename — fix or ignore, your call | +| `[QUESTION]` | Reviewer is uncertain — needs clarification from the author | + +--- + +## What to Check + +### Correctness +- Off-by-one errors, null/undefined handling, edge cases not covered +- Incorrect assumptions about input ranges or types +- Race conditions, mutation of shared state + +### Security +- Unsanitized inputs, injection vectors (SQL, shell, XSS) +- Secrets or credentials hardcoded or logged +- Overly permissive access control + +### Performance +- N+1 queries, unnecessary re-renders, blocking calls in hot paths +- Unbounded loops or allocations + +### Maintainability +- Functions doing more than one thing +- Magic numbers or strings without named constants +- Deeply nested logic that can be flattened +- Missing or misleading comments on non-obvious logic + +### Tests +- Are new code paths covered? +- Are edge cases and failure modes tested? +- Are tests actually asserting meaningful behavior? + +--- + +## Model-Specific Guidance + +| Model | Strength | Best For | +|---|---|---| +| `devstral-small-2:24b` | Code reasoning, diff analysis | Line-level bugs, logic errors | +| `gemma4:31b` | Broad reasoning | Architecture-level feedback, abstractions | +| `qwen3:27b` | Structured output | Generating formatted review summaries | + +--- + +## Output Format + +Structure your review exactly like this: + +``` +## Intent +[One sentence describing what the code does] + +## Findings + +### `path/to/file.ext` +- [SEVERITY] Line X: . Suggestion: + +### `path/to/other.ext` +- [SEVERITY] Lines X–Y: . Suggestion: + +## Summary +**Verdict:** Approve / Request Changes / Needs Discussion + +**Must fix before merge:** +1. ... +2. ... + +**Nice to have:** +- ... +``` + +--- + +## Constraints + +- **Do not rewrite the code** unless asked — suggest, don't replace. +- **Do not invent bugs** — only flag what you can verify from the actual code shown. +- **Do not nitpick everything** — if there are `[CRITICAL]` or `[MAJOR]` issues, lead with those; don't bury them in `[NIT]`s. +- If you lack context (e.g., external dependencies, DB schema), say so with a `[QUESTION]` rather than guessing. +- Keep each finding to 2–3 lines max. Be dense, not verbose. diff --git a/src/chat-view.ts b/src/chat-view.ts new file mode 100644 index 0000000..a3e8d59 --- /dev/null +++ b/src/chat-view.ts @@ -0,0 +1,432 @@ +import { ItemView, WorkspaceLeaf, Notice, TFile } from 'obsidian'; +/// +// Use global types from JSDOM setup +type KeyboardEvent = globalThis.KeyboardEvent; +type HTMLTextAreaElement = globalThis.HTMLTextAreaElement; +type HTMLButtonElement = globalThis.HTMLButtonElement; + +const DEFAULT_VAULT_SEARCH_LIMIT = 3; +const MAX_MESSAGE_HISTORY = 50; +import { + PluginSettings, + OllamaMessage, + ChatMessage, + OllamaTool, + ToolCall, + ToolResult, +} from './types'; +import { OllamaClient } from './ollama-client'; +import { VaultIndexer } from './vault-indexer'; +import { ToolExecutor } from './tool-executor'; +import { ErrorHandler } from './error-handler'; + +export class ChatView extends ItemView { + private settings: PluginSettings; + private messages: ChatMessage[] = []; + private ollamaClient: OllamaClient; + private vaultIndexer: VaultIndexer; + private toolExecutor: ToolExecutor; + private lastMessageEl: HTMLElement | null = null; + private newChatButton: HTMLElement | null = null; + private sendButton: HTMLElement | null = null; + private inputEl: HTMLElement | null = null; + private chatContainer: HTMLElement | null = null; + private sendButtonClickHandler: (() => Promise) | null = null; + private inputKeyDownHandler: ((e: KeyboardEvent) => Promise) | null = null; + private newChatButtonClickHandler: (() => void) | null = null; + private listenersAttached = false; + + constructor(leaf: WorkspaceLeaf, settings: PluginSettings) { + super(leaf); + this.settings = settings; + this.ollamaClient = new OllamaClient(settings.ollamaUrl, settings.model); + this.vaultIndexer = new VaultIndexer(this.app.vault); + this.toolExecutor = new ToolExecutor(this.app.vault, this.app); + } + + getViewType(): string { + return 'ollama-chat-view'; + } + + getDisplayText(): string { + return 'Ollama Chat'; + } + + async onOpen() { + await this.render(); + this.removeEventListeners(); // Clean up any existing listeners before reattaching + this.setupEventListeners(); + } + + async onClose() { + this.ollamaClient.cancelStream(); + this.removeEventListeners(); + this.cleanupStreamingResources(); + this.lastMessageEl = null; + this.sendButton = null; + this.inputEl = null; + this.chatContainer = null; + } + + private cleanupStreamingResources(): void { + // Ensure any ongoing streaming is properly cleaned up + if (this.lastMessageEl && this.lastMessageEl.parentElement) { + this.lastMessageEl.parentElement.removeChild(this.lastMessageEl); + this.lastMessageEl = null; + } + } + + async render() { + const container = + this.chatContainer || this.contentEl.createEl('div', { cls: 'ollama-chat-container' }); + this.chatContainer = container; + const inputContainer = + this.contentEl.querySelector('.ollama-input-container') || + this.contentEl.createEl('div', { cls: 'ollama-input-container' }); + + if (!this.inputEl) { + this.inputEl = inputContainer.createEl('textarea', { cls: 'ollama-input' }); + } + if (!this.sendButton) { + this.sendButton = inputContainer.createEl('button', { + cls: 'ollama-send-button', + }) as HTMLButtonElement; + (this.sendButton as HTMLButtonElement).textContent = 'Send'; + } + + if (!this.newChatButton) { + const newChatContainer = + this.contentEl.querySelector('.ollama-new-chat') || + this.contentEl.createEl('div', { cls: 'ollama-new-chat' }); + this.newChatButton = newChatContainer.createEl('button', { + cls: 'ollama-new-chat-button', + }) as HTMLButtonElement; + (this.newChatButton as HTMLButtonElement).textContent = '🔄 New Chat'; + (this.newChatButton as HTMLButtonElement).title = 'Start a new conversation'; + } + + // Create immutable snapshot for rendering + const messagesSnapshot = [...this.messages]; + + // Only render messages that are not currently streaming + const nonStreamingMessages = messagesSnapshot.filter((msg) => !msg.isStreaming); + + // Differential update: only update messages that have changed + const existingMessages = container.querySelectorAll('.ollama-message'); + const existingIds = Array.from(existingMessages).map((el) => el.getAttribute('data-msg-id')); + + for (const msg of nonStreamingMessages) { + const existingEl = container.querySelector( + `.ollama-message[data-msg-id="${msg.id}"]` + ) as HTMLElement | null; + if (existingEl) { + existingEl.textContent = msg.content; + } else { + const messageEl = container.createEl('div', { + cls: `ollama-message ${msg.role}`, + }) as HTMLElement; + messageEl.setAttribute('data-msg-id', msg.id); + messageEl.textContent = msg.content; + } + } + + // Remove messages that are no longer in the array + for (const el of Array.from(existingMessages)) { + const id = el.getAttribute('data-msg-id'); + if (!id || !nonStreamingMessages.some((m) => m.id === id)) { + el.remove(); + } + } + + // Re-attach streaming message if it exists + const streamingMessage = messagesSnapshot.find((msg) => msg.isStreaming); + if (streamingMessage && this.lastMessageEl) { + const existingStreamingEl = container.querySelector( + `.ollama-message[data-msg-id="${streamingMessage.id}"]` + ); + if (!existingStreamingEl) { + container.appendChild(this.lastMessageEl); + } + } + } + + private setupEventListeners(): void { + if (!this.sendButton || !this.inputEl || this.listenersAttached) return; + + // Create handlers if they don't exist + if (!this.sendButtonClickHandler) { + this.sendButtonClickHandler = async () => { + if (!this.inputEl) return; + await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value); + (this.inputEl as HTMLTextAreaElement).value = ''; + }; + } + + if (!this.inputKeyDownHandler) { + this.inputKeyDownHandler = async (e: KeyboardEvent) => { + if (!this.inputEl || e.key !== 'Enter' || e.shiftKey) return; + e.preventDefault(); + await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value); + (this.inputEl as HTMLTextAreaElement).value = ''; + }; + } + + // Add event listeners + (this.sendButton as HTMLButtonElement).addEventListener('click', this.sendButtonClickHandler!); + (this.inputEl as HTMLTextAreaElement).addEventListener('keydown', this.inputKeyDownHandler!); + if (this.newChatButton) { + if (!this.newChatButtonClickHandler) { + this.newChatButtonClickHandler = () => this.clearConversation(); + } + (this.newChatButton as HTMLButtonElement).addEventListener( + 'click', + this.newChatButtonClickHandler! + ); + } + this.listenersAttached = true; + } + + private removeEventListeners(): void { + if (this.sendButton && this.sendButtonClickHandler) { + (this.sendButton as HTMLButtonElement).removeEventListener( + 'click', + this.sendButtonClickHandler! + ); + } + if (this.inputEl && this.inputKeyDownHandler) { + (this.inputEl as HTMLTextAreaElement).removeEventListener( + 'keydown', + this.inputKeyDownHandler! + ); + } + if (this.newChatButton && this.newChatButtonClickHandler) { + (this.newChatButton as HTMLButtonElement).removeEventListener( + 'click', + this.newChatButtonClickHandler! + ); + } + this.listenersAttached = false; + } + + private clearConversation(): void { + // Create new array to ensure immutability + this.messages = []; + this.lastMessageEl = null; + this.render(); + new Notice('Conversation cleared'); + } + + private updateMessageById(id: string, partial: Partial): boolean { + const index = this.messages.findIndex((m) => m.id === id); + if (index < 0) return false; + this.messages = [ + ...this.messages.slice(0, index), + { ...this.messages[index], ...partial }, + ...this.messages.slice(index + 1), + ]; + return true; + } + + private async updateLastMessage(content: string) { + const streamingMessage = this.messages.find((msg) => msg.isStreaming); + if (streamingMessage && !this.lastMessageEl) { + this.lastMessageEl = this.contentEl.createEl('div', { + cls: `ollama-message assistant`, + }) as HTMLElement; + this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id); + } + if (this.lastMessageEl) { + this.lastMessageEl.textContent = content; + } + } + + private async handleUserInput(content: string) { + if (!this.sendButton || !this.inputEl) return; + (this.sendButton as HTMLButtonElement).disabled = true; + + try { + // Guard against empty messages + const userMessage = content.trim(); + if (!userMessage) return; + + // Search vault using user message as query + const entries = await this.vaultIndexer.searchVault(userMessage, DEFAULT_VAULT_SEARCH_LIMIT); + let context = entries.map((e) => `### ${e.title}\n${e.content}`).join('\n\n'); + + // Cap context size to prevent prompt bloat with large vaults + const MAX_CONTEXT_LENGTH = 4000; + if (context.length > MAX_CONTEXT_LENGTH) { + context = context.substring(0, MAX_CONTEXT_LENGTH) + '\n\n... (truncated)'; + } + + const systemMessage: OllamaMessage = { + role: 'system', + content: 'You are a helpful assistant.', + }; + const userMessageWithContext: OllamaMessage = { + role: 'user', + content: `${context}\n\n${userMessage}`, + }; + + const messages: OllamaMessage[] = [ + systemMessage, + ...this.messages.map( + (m) => + ({ + role: m.role, + content: m.content, + tool_calls: m.tool_calls, + }) as OllamaMessage + ), + userMessageWithContext, + ]; + + const tools: OllamaTool[] = [ + { + type: 'function', + function: { + name: 'create_file', + description: 'Create a new file in the vault', + parameters: { + type: 'object' as const, + properties: { + path: { type: 'string' as const }, + content: { type: 'string' as const }, + }, + required: ['path', 'content'], + }, + }, + }, + ]; + + const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + const userMessageId = messageId; + const assistantMessageId = `${messageId}-assistant`; + + // Store user message in conversation history + const userChatMessage: ChatMessage = { + id: userMessageId, + role: 'user' as const, + content: userMessage, + timestamp: Date.now(), + }; + + const assistantMessage: ChatMessage = { + id: assistantMessageId, + role: 'assistant' as const, + content: '', + timestamp: Date.now(), + isStreaming: true, + }; + + // Update messages immutably + this.messages = [...this.messages, userChatMessage, assistantMessage]; + + await this.render(); + + const stream = await this.ollamaClient.streamChat(messages, tools); + let fullResponse = ''; + let toolCalls: ToolCall[] = []; + let chunkCount = 0; + const MAX_STREAM_CHUNKS = 1000; + const maxChunks = MAX_STREAM_CHUNKS; + + try { + for await (const chunk of stream) { + chunkCount++; + if (chunkCount > maxChunks) { + throw new Error('Response too long, stopped streaming'); + } + + if (chunk.content) { + fullResponse += chunk.content; + } + + if (chunk.tool_calls) { + toolCalls = toolCalls.concat(chunk.tool_calls); + } + + await this.updateLastMessage(fullResponse); + } + } finally { + // Clean up streaming resources regardless of outcome + this.cleanupStreamingResources(); + } + + // Update the assistant message with the full response immutably + if ( + !this.updateMessageById(assistantMessageId, { + content: fullResponse, + tool_calls: toolCalls, + }) + ) { + throw new Error('Assistant message not found'); + } + + // Process tool calls with proper follow-up context + if (toolCalls.length > 0) { + // Validate tool calls before processing + const MAX_TOOL_CALLS = 10; + if (toolCalls.length > MAX_TOOL_CALLS) { + throw new Error(`Too many tool calls (max ${MAX_TOOL_CALLS})`); + } + + // Collect all tool results using allSettled to support partial results + const settledResults = await Promise.allSettled( + toolCalls.map((call) => this.toolExecutor.handleToolCall(call)) + ); + + let toolResults: ToolResult[] = []; + for (const result of settledResults) { + if (result.status === 'fulfilled') { + toolResults.push(result.value); + } else { + // Use centralized error handler for tool errors + ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput'); + } + } + + // Create follow-up messages including the assistant's tool calls and results + const followUpMessages: OllamaMessage[] = [ + ...messages, + { role: 'assistant' as const, content: fullResponse, tool_calls: toolCalls }, + ...toolResults.map((result) => ({ + role: 'tool' as const, + content: JSON.stringify(result), + })), + ]; + + const followUp = await this.ollamaClient.chat(followUpMessages, tools); + fullResponse += followUp.content; + await this.updateLastMessage(fullResponse); + + // Update the assistant message with the final response immutably + this.updateMessageById(assistantMessageId, { content: fullResponse, isStreaming: false }); + } + + // Update last message immutably — only if no tool calls were processed + if (toolCalls.length === 0) { + const lastMessageIndex = this.messages.length - 1; + if (lastMessageIndex >= 0) { + const lastMessage = { ...this.messages[lastMessageIndex], isStreaming: false }; + this.messages = [...this.messages.slice(0, lastMessageIndex), lastMessage]; + } + } + + // Limit conversation history to prevent memory issues + if (this.messages.length > MAX_MESSAGE_HISTORY) { + this.messages = this.messages.slice(-MAX_MESSAGE_HISTORY); + } + await this.render(); + } catch (error) { + // Use centralized error handler + ErrorHandler.handleError(error, 'ChatView.handleUserInput'); + this.cleanupStreamingResources(); + } finally { + if (this.sendButton) { + (this.sendButton as HTMLButtonElement).disabled = false; + } + } + } +} diff --git a/src/error-handler.ts b/src/error-handler.ts new file mode 100644 index 0000000..441f0ea --- /dev/null +++ b/src/error-handler.ts @@ -0,0 +1,178 @@ +import { Notice } from 'obsidian'; +import { + OllamaError, + ErrorType, + NetworkError, + ApiError, + ValidationError, + StreamingError, + ToolExecutionError, + PathValidationError, +} from './types'; + +export class ErrorHandler { + /** + * Centralized error handling for the Ollama plugin + * Provides consistent error messages and logging + */ + static handleError(error: unknown, context?: string): void { + let userMessage = 'An unexpected error occurred'; + let shouldShowError = true; + + if (error instanceof OllamaError) { + userMessage = this.getUserFriendlyMessage(error); + shouldShowError = true; + } else if (error instanceof Error) { + userMessage = this.getUserFriendlyMessageFromError(error); + shouldShowError = true; + } else { + userMessage = 'An unexpected error occurred'; + shouldShowError = true; + } + + if (shouldShowError) { + new Notice(userMessage); + } + + // Log detailed error for debugging + console.error( + `[OllamaPlugin${context ? ' ' + context : ''}] ${error instanceof Error ? error.message : 'Unknown error'}` + ); + if (error instanceof Error) { + console.error('[Stack]', error.stack); + } + } + + /** + * Get user-friendly message from specific error types + */ + private static getUserFriendlyMessage(error: OllamaError): string { + switch (error.type) { + case ErrorType.NETWORK_ERROR: + if (error instanceof NetworkError) { + return 'Connection error. Please check if Ollama is running.'; + } + return 'Network error. Please check your connection to Ollama.'; + + case ErrorType.API_ERROR: + if (error instanceof ApiError) { + return 'Ollama API error. Please check the Ollama logs for details.'; + } + return 'API communication error. Please try again.'; + + case ErrorType.VALIDATION_ERROR: + if (error instanceof ValidationError) { + const details = error.validationDetails; + if (details?.field) { + return `Invalid ${details.field}. ${details.message || 'Please check your input.'}`; + } + return 'Input validation error. Please correct your input.'; + } + return 'Input validation error. Please correct your input.'; + + case ErrorType.STREAMING_ERROR: + if (error instanceof StreamingError) { + return 'Response too long. Please try a shorter request.'; + } + return 'Streaming error. Please try again.'; + + case ErrorType.TOOL_EXECUTION_ERROR: + if (error instanceof ToolExecutionError) { + return `Tool error: ${error.toolName || 'tool'} failed to execute. Please try again.`; + } + return 'Tool execution error. Please try a different command.'; + + case ErrorType.PATH_VALIDATION_ERROR: + if (error instanceof PathValidationError) { + return 'Invalid file path. Please use a relative path without special characters.'; + } + return 'Path validation error. Please check your file path.'; + + case ErrorType.UNKNOWN_ERROR: + return 'An unexpected error occurred. Please try again.'; + + default: + return error.message || 'An error occurred'; + } + } + + /** + * Get user-friendly message from generic Error + */ + /** + * Get user-friendly message from generic Error + * Note: This method uses substring matching which is inherently fragile. + * If an error message happens to contain certain keywords but isn't actually + * that type of error, it may be misclassified. This heuristic approach + * provides a good balance between robustness and accuracy for most common cases. + */ + private static getUserFriendlyMessageFromError(error: Error): string { + const message = error.message.toLowerCase(); + + if (message.includes('timeout')) { + return 'Request timed out. Please check your Ollama connection.'; + } + + if ( + message.includes('network') || + message.includes('fetch') || + message.includes('connection') + ) { + return 'Connection error. Please check if Ollama is running.'; + } + + if (message.includes('validation') || message.includes('format')) { + return 'Invalid input. Please check your message.'; + } + + if (message.includes('stream') || message.includes('chunk')) { + return 'Response too long. Please try a shorter request.'; + } + + if (message.includes('tool') || message.includes('function')) { + return 'Tool execution error. Please try a different command.'; + } + + if (message.includes('path') || message.includes('file')) { + return 'Invalid file path. Please use a relative path without special characters.'; + } + + return error.message; + } + + /** + * Create specific error instances from different error types + */ + static createNetworkError(message: string, statusCode?: number): NetworkError { + return new NetworkError(message, statusCode); + } + + static createApiError(message: string, apiError?: any): ApiError { + return new ApiError(message, apiError); + } + + static createValidationError( + message: string, + field?: string, + details?: Record + ): ValidationError { + const validationDetails = field ? { field, message } : details; + return new ValidationError(message, validationDetails); + } + + static createStreamingError(message: string, chunkDetails?: any): StreamingError { + return new StreamingError(message, chunkDetails); + } + + static createToolExecutionError(message: string, toolName?: string): ToolExecutionError { + return new ToolExecutionError(message, toolName); + } + + static createPathValidationError(message: string, invalidPath?: string): PathValidationError { + return new PathValidationError(message, invalidPath); + } + + static createUnknownError(message: string): OllamaError { + return new OllamaError(message, ErrorType.UNKNOWN_ERROR); + } +} diff --git a/src/ollama-client.ts b/src/ollama-client.ts new file mode 100644 index 0000000..fefd84f --- /dev/null +++ b/src/ollama-client.ts @@ -0,0 +1,207 @@ +import { OllamaMessage, OllamaTool, ToolCall } from './types'; + +interface FetchResponse { + ok: boolean; + status: number; + headers?: { + get: (name: string) => string | null; + }; + body?: { + getReader: () => ReadableStreamDefaultReader; + } | null; + json?: () => Promise; +} + +interface FetchOptions { + method: string; + headers: Record; + body: string; + signal?: AbortSignal; +} + +export class OllamaClient { + private url: string; + private model: string; + private abortController: AbortController | null = null; + + // Mock fetch function for testing + private fetchFn: typeof fetch = fetch; + + constructor(url: string, model: string, fetchFn?: typeof fetch) { + this.url = url; + this.model = model; + if (fetchFn) this.fetchFn = fetchFn; + } + + async streamChat( + messages: OllamaMessage[], + tools: OllamaTool[], + timeoutMs: number = 60000 + ): Promise> { + this.abortController = new AbortController(); + + const timeoutId = setTimeout(() => { + this.abortController?.abort(); + }, timeoutMs); + + let response: FetchResponse; + try { + response = await this.fetchFn(`${this.url}/api/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: this.model, + messages, + tools, + stream: true, + }), + signal: this.abortController.signal, + } as FetchOptions); + } catch (fetchError: any) { + clearTimeout(timeoutId); + this.abortController = null; + if (fetchError.name === 'AbortError' || fetchError.code === 'ABORT_ERR') { + throw new Error('Request timeout while connecting to Ollama'); + } + throw fetchError; + } + + if (!response.ok) { + throw new Error(`Ollama API error: ${response.status}`); + } + + if (!response.body) { + throw new Error('No response body'); + } + + // Validate response structure + const contentType = response.headers?.get('content-type'); + if (!contentType?.match(/application\/(x-ndjson|json)/)) { + throw new Error('Invalid response format'); + } + + const reader = response.body.getReader(); + + const self = this; + return { + [Symbol.asyncIterator]: async function* () { + const decoder = new TextDecoder(); + let buffer = ''; + let chunkCount = 0; + let skippedChunks = 0; + const maxChunks = 1000; // Safety limit + const maxSkipped = 50; // Fail if too many chunks are malformed + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (++chunkCount > maxChunks) { + throw new Error('Response too long, stopped streaming'); + } + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + for (const line of lines) { + if (line.trim() === '') continue; + try { + const data = JSON.parse(line); + if (data.message && typeof data.message === 'object') { + // Validate message structure + if (data.message.error && typeof data.message.error === 'string') { + throw new Error(`Ollama error: ${data.message.error}`); + } + yield { + content: data.message.content || '', + tool_calls: Array.isArray(data.message.tool_calls) + ? data.message.tool_calls + : [], + }; + } + } catch (parseError) { + // Check if this is an Ollama error (thrown intentionally) vs a parse error + if (parseError instanceof Error && parseError.message.startsWith('Ollama error:')) { + // This is an intentional Ollama error, re-throw it + self.abortController = null; + reader.releaseLock(); + throw parseError; + } + + // This is a parse error, skip the malformed chunk + skippedChunks++; + console.warn( + `[OllamaClient] Skipped malformed chunk ${skippedChunks}/${maxSkipped}:`, + parseError instanceof Error ? parseError.message : String(parseError) + ); + if (skippedChunks > maxSkipped) { + throw new Error( + `Too many malformed response chunks (${skippedChunks}). Connection may be degraded.` + ); + } + // Skip invalid chunks but continue streaming + continue; + } + } + } + } catch (streamError) { + self.abortController = null; + if (streamError instanceof Error && streamError.name === 'AbortError') { + throw new Error('Streaming request was cancelled'); + } + throw streamError; + } finally { + reader.releaseLock(); + self.abortController = null; + } + }, + }; + } + + async chat( + messages: OllamaMessage[], + tools: OllamaTool[], + timeoutMs: number = 30000 + ): Promise<{ content: string; tool_calls?: ToolCall[] }> { + const abortController = new AbortController(); + const timeoutId = setTimeout(() => { + abortController.abort(); + }, timeoutMs); + + const response = await this.fetchFn(`${this.url}/api/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: this.model, + messages, + tools, + stream: false, + }), + signal: abortController.signal, + } as FetchOptions); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`Ollama API error: ${response.status}`); + } + + const responseData = await response.json(); + + const data = responseData; + const messageData = data.message; + return { + content: messageData?.content || '', + tool_calls: messageData?.tool_calls || [], + }; + } + + cancelStream(): void { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + } +} diff --git a/src/tool-executor.ts b/src/tool-executor.ts new file mode 100644 index 0000000..05a1ba3 --- /dev/null +++ b/src/tool-executor.ts @@ -0,0 +1,60 @@ +import { Vault, TFile, Notice, App } from 'obsidian'; +import { ToolCall, ToolResult, ToolExecutionError, PathValidationError } from './types'; +import { validatePath } from './utils'; + +export class ToolExecutor { + private vault: Vault; + private app: App; + + constructor(vault: Vault, app: App) { + this.vault = vault; + this.app = app; + } + + async handleToolCall(call: ToolCall): Promise { + const { + function: { name, arguments: args }, + } = call; + + switch (name) { + case 'create_file': { + let filePath: string, content: string; + try { + // Handle both string (JSON) and object arguments, since some Ollama versions return args as an object + const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args; + filePath = parsedArgs.path; + content = parsedArgs.content; + } catch (e) { + throw new ToolExecutionError( + `Invalid arguments provided for create_file: ${e instanceof Error ? e.message : 'Unknown parsing error'}`, + 'create_file' + ); + } + + // Validate content is a string + if (typeof content !== 'string') { + throw new ToolExecutionError('Content must be a string', 'create_file'); + } + + // Validate path using shared utility + if (typeof filePath !== 'string') { + throw new ToolExecutionError('Path must be a string', 'create_file'); + } + + if (!filePath) { + throw new ToolExecutionError('Path is required', 'create_file'); + } + + const pathValidation = validatePath(filePath); + if (!pathValidation.valid) { + throw new PathValidationError(pathValidation.error || 'Path validation failed', filePath); + } + + await this.vault.create(filePath, content); + return { success: true, message: 'File created successfully' }; + } + default: + return { success: false, message: `Unknown tool: ${name}` }; + } + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..52079e9 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,134 @@ +export interface PluginSettings { + ollamaUrl: string; + model: string; + lastIndexTime: number; +} + +export enum ErrorType { + NETWORK_ERROR = 'network_error', + API_ERROR = 'api_error', + VALIDATION_ERROR = 'validation_error', + STREAMING_ERROR = 'streaming_error', + TOOL_EXECUTION_ERROR = 'tool_execution_error', + PATH_VALIDATION_ERROR = 'path_validation_error', + UNKNOWN_ERROR = 'unknown_error', +} + +export class OllamaError extends Error { + constructor( + message: string, + public readonly type: ErrorType, + public readonly details?: Record + ) { + super(message); + this.name = 'OllamaError'; + } +} + +export class NetworkError extends OllamaError { + constructor( + message: string, + public readonly statusCode?: number + ) { + super(message, ErrorType.NETWORK_ERROR, { statusCode }); + this.name = 'NetworkError'; + } +} + +export class ApiError extends OllamaError { + constructor( + message: string, + public readonly apiError?: any + ) { + super(message, ErrorType.API_ERROR, { apiError }); + this.name = 'ApiError'; + } +} + +export class ValidationError extends OllamaError { + constructor( + message: string, + public readonly validationDetails?: Record + ) { + super(message, ErrorType.VALIDATION_ERROR, validationDetails); + this.name = 'ValidationError'; + } +} + +export class StreamingError extends OllamaError { + constructor( + message: string, + public readonly chunkDetails?: any + ) { + super(message, ErrorType.STREAMING_ERROR, chunkDetails); + this.name = 'StreamingError'; + } +} + +export class ToolExecutionError extends OllamaError { + constructor( + message: string, + public readonly toolName?: string + ) { + super(message, ErrorType.TOOL_EXECUTION_ERROR, { toolName }); + this.name = 'ToolExecutionError'; + } +} + +export class PathValidationError extends OllamaError { + constructor( + message: string, + public readonly invalidPath?: string + ) { + super(message, ErrorType.PATH_VALIDATION_ERROR, { invalidPath }); + this.name = 'PathValidationError'; + } +} + +export interface OllamaMessage { + role: 'system' | 'user' | 'assistant' | 'tool'; + content: string; + tool_calls?: ToolCall[]; +} + +export interface ToolCall { + function: { + name: string; + arguments: string | Record; + }; +} + +export interface OllamaTool { + type: 'function'; + function: { + name: string; + description: string; + parameters: { + type: 'object'; + properties: Record; + required: string[]; + }; + }; +} + +export interface ToolResult { + success: boolean; + message: string; + // Adding optional details field for better error reporting + details?: Record; +} + +export interface VaultIndexEntry { + title: string; + content: string; + score: number; +} + +export interface ChatMessage { + id: string; + role: 'user' | 'assistant' | 'tool'; + content: string; + timestamp: number; + isStreaming?: boolean; + tool_calls?: ToolCall[]; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..983d576 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,52 @@ +/** + * Normalizes file paths for browser/ Obsidian environment + * Replaces multiple slashes with single slash and handles forward/backward slashes + */ +export function normalizePath(path: string): string { + // Replace multiple slashes with single slash + let normalized = path.replace(/[\\\/]+/g, '/'); + + // Remove trailing slash unless it's the root + if (normalized.length > 1 && normalized.endsWith('/')) { + normalized = normalized.slice(0, -1); + } + + return normalized; +} + +/** + * Validates a path string for safety (no traversal, no absolute paths, no invalid chars) + */ +export function validatePath(path: string): { valid: boolean; error?: string } { + const normalized = normalizePath(path); + + // Check for path traversal by looking for .. as a path segment (not just substring in filenames) + const segments = normalized.split('/'); + if (segments.includes('..')) { + return { valid: false, error: 'Path traversal not allowed' }; + } + + // Check if absolute path + if (normalized.startsWith('/') || normalized.startsWith('\\')) { + return { valid: false, error: 'Absolute paths not allowed' }; + } + + // Check for windows drive letters + if (/^[a-zA-Z]:/.test(normalized)) { + return { valid: false, error: 'Absolute paths not allowed' }; + } + + // Check for invalid characters + const invalidChars = /[\<\>\:\"\|\\\?\*~]/; + if (invalidChars.test(path)) { + return { valid: false, error: 'Path contains illegal characters' }; + } + + // Check path length + const MAX_PATH_LENGTH = 200; + if (path.length > MAX_PATH_LENGTH) { + return { valid: false, error: 'Path too long' }; + } + + return { valid: true }; +} diff --git a/src/vault-indexer.ts b/src/vault-indexer.ts new file mode 100644 index 0000000..f2917e1 --- /dev/null +++ b/src/vault-indexer.ts @@ -0,0 +1,397 @@ +import { Vault, TFile } from 'obsidian'; +import { VaultIndexEntry } from './types'; + +const MAX_CONTENT_PREVIEW_LENGTH = 500; +const BATCH_SIZE = 10; +const MAX_TOKENS = 10000; +const TITLE_WEIGHT = 10; +const HEADING_WEIGHT = 5; +const FRONTMATTER_WEIGHT = 8; +const FIRST_PARAGRAPH_WEIGHT = 3; +const BODY_WEIGHT = 1; +const PHRASE_MATCH_BONUS = 2; +const EXACT_WORD_MATCH_BONUS = 1.5; + +interface TokenizedContent { + text: string; + tokens: string[]; + title: string; + titleTokens: string[]; + headings: string[]; + headingTokens: string[][]; + frontmatter: Record; + frontmatterTokens: string[]; + firstParagraph: string; + firstParagraphTokens: string[]; +} + +export class VaultIndexer { + private vault: Vault; + + constructor(vault: Vault) { + this.vault = vault; + } + + async searchVault(query: string, limit: number = 5): Promise { + const files = this.vault.getMarkdownFiles(); + const results: VaultIndexEntry[] = []; + + const queryTokens = this.tokenize(query); + if (queryTokens.length === 0) { + return []; + } + + // Process files in batches with concurrency limit + for (let i = 0; i < files.length; i += BATCH_SIZE) { + const batch = files.slice(i, i + BATCH_SIZE); + const batchResults = await Promise.allSettled( + batch.map(async (file) => { + const fullContent = await this.vault.read(file); + const tokenized = this.tokenizeContent(fullContent, file); + const score = this.calculateWeightedScore(tokenized, query, queryTokens); + + if (score > 0) { + return { + title: file.basename, + content: fullContent.substring(0, MAX_CONTENT_PREVIEW_LENGTH), + score, + } as VaultIndexEntry; + } + return null; + }) + ); + + for (const result of batchResults) { + if (result.status === 'fulfilled' && result.value !== null) { + results.push(result.value); + } else if (result.status === 'rejected') { + console.warn( + `[VaultIndexer] Failed to read file: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}` + ); + } + } + } + + return results.sort((a, b) => b.score - a.score).slice(0, limit); + } + + /** + * Tokenize a string into lowercase words, filtering out stop words and very short tokens + */ + private tokenize(text: string): string[] { + return text + .toLowerCase() + .replace(/[^\w\s]/g, ' ') + .split(/\s+/) + .filter((token) => { + // Filter out common stop words and very short tokens + const stopWords = new Set([ + 'a', + 'an', + 'the', + 'and', + 'or', + 'but', + 'in', + 'on', + 'at', + 'to', + 'for', + 'of', + 'with', + 'by', + 'is', + 'are', + 'was', + 'were', + 'be', + 'been', + 'have', + 'has', + 'had', + 'do', + 'does', + 'did', + 'will', + 'would', + 'could', + 'should', + 'may', + 'might', + 'must', + 'shall', + 'can', + 'need', + 'dare', + 'ought', + 'used', + 'it', + 'its', + 'this', + 'that', + 'these', + 'those', + 'i', + 'you', + 'he', + 'she', + 'we', + 'they', + 'me', + 'him', + 'her', + 'us', + 'them', + 'my', + 'your', + 'his', + 'our', + 'their', + 'mine', + 'yours', + 'hers', + 'ours', + 'theirs', + 'what', + 'which', + 'who', + 'whom', + 'whose', + 'where', + 'when', + 'why', + 'how', + 'not', + 'no', + 'nor', + 'so', + 'if', + 'then', + 'than', + 'too', + 'very', + 'just', + 'about', + 'above', + 'after', + 'again', + 'all', + 'am', + 'any', + 'as', + 'because', + 'before', + 'being', + 'below', + 'between', + 'both', + 'during', + 'each', + 'few', + 'further', + 'get', + 'got', + 'here', + 'into', + 'more', + 'most', + 'much', + 'myself', + 'nothing', + 'only', + 'other', + 'out', + 'over', + 'own', + 'same', + 'some', + 'such', + 'there', + 'through', + 'under', + 'until', + 'up', + 'while', + 'why', + 'yes', + 'also', + 'from', + ]); + return token.length > 1 && !stopWords.has(token); + }); + } + + /** + * Tokenize markdown content into structured components + */ + private tokenizeContent(content: string, file: TFile): TokenizedContent { + const title = file.basename; + const titleTokens = this.tokenize(title); + + // Extract headings + const headingRegex = /^(#{1,6})\s+(.+)$/gm; + const headings: string[] = []; + let match: RegExpExecArray | null; + const headingRegexState = /^(#{1,6})\s+(.+)$/gm; + + while ((match = headingRegexState.exec(content)) !== null) { + headings.push(match[2]); + } + + // Extract frontmatter (YAML between --- markers) + const frontmatter: Record = {}; + const frontmatterRegex = /^---\n([\s\S]*?)\n---/; + const frontmatterMatch = frontmatterRegex.exec(content); + if (frontmatterMatch) { + const frontmatterContent = frontmatterMatch[1]; + const lines = frontmatterContent.split('\n'); + for (const line of lines) { + const colonIndex = line.indexOf(':'); + if (colonIndex > 0) { + const key = line.substring(0, colonIndex).trim(); + const value = line.substring(colonIndex + 1).trim(); + frontmatter[key] = value; + } + } + } + + // Get frontmatter tokens from values + const frontmatterTokens = this.tokenize(Object.values(frontmatter).join(' ')); + + // Extract first paragraph (non-empty lines after frontmatter, stop at paragraph break) + const cleanContent = content.replace(/^---\n[\s\S]*?\n---/, '').trim(); + const lines = cleanContent.split('\n'); + let firstParagraph = ''; + for (const line of lines) { + const trimmed = line.trim(); + // Stop at empty line (paragraph break) + if (!trimmed) { + break; + } + // Skip headings + if (trimmed.startsWith('#')) { + continue; + } + firstParagraph += trimmed + ' '; + if (firstParagraph.length > 200) { + break; + } + } + const firstParagraphTokens = this.tokenize(firstParagraph); + + // Get body tokens (limit to prevent memory issues with huge files) + const bodyText = cleanContent.substring(0, MAX_TOKENS); + const tokens = this.tokenize(bodyText); + + return { + text: bodyText, + tokens, + title, + titleTokens, + headings, + headingTokens: headings.map((h) => this.tokenize(h)), + frontmatter, + frontmatterTokens, + firstParagraph, + firstParagraphTokens, + }; + } + + /** + * Calculate a weighted score based on where query tokens appear + * Uses a combination of position weighting, exact matching, and phrase matching + */ + private calculateWeightedScore( + tokenized: TokenizedContent, + query: string, + queryTokens: string[] + ): number { + let score = 0; + const queryLower = query.toLowerCase(); + const contentLower = tokenized.text.toLowerCase(); + const contentWithBoundaries = '\\b' + contentLower + '\\b'; + + // Check for exact phrase match (bonus) + if (queryLower.length > 0 && contentLower.includes(queryLower)) { + score += PHRASE_MATCH_BONUS * queryTokens.length; + } + + for (const queryToken of queryTokens) { + let tokenScore = 0; + + // Title match (highest priority) + if (tokenized.titleTokens.some((t) => this.exactMatch(t, queryToken))) { + tokenScore += TITLE_WEIGHT; + } + + // Frontmatter match (high priority - often contains tags/categories) + if (tokenized.frontmatterTokens.some((t) => this.exactMatch(t, queryToken))) { + tokenScore += FRONTMATTER_WEIGHT; + } + + // Heading match (high priority) + for (const headingTokens of tokenized.headingTokens) { + if (headingTokens.some((t) => this.exactMatch(t, queryToken))) { + tokenScore += HEADING_WEIGHT; + break; // Only count once per query token + } + } + + // First paragraph match (medium priority - likely contains topic summary) + if (tokenized.firstParagraphTokens.some((t) => this.exactMatch(t, queryToken))) { + tokenScore += FIRST_PARAGRAPH_WEIGHT; + } + + // Body match (lowest priority) + const bodyMatchCount = tokenized.tokens.filter((t) => this.exactMatch(t, queryToken)).length; + if (bodyMatchCount > 0) { + // Use logarithmic scaling to prevent very frequent words from dominating + tokenScore += BODY_WEIGHT * Math.log(1 + bodyMatchCount); + } + + // Exact word boundary bonus + if (new RegExp(`\\b${queryToken}\\b`).test(contentLower)) { + tokenScore *= EXACT_WORD_MATCH_BONUS; + } + + score += tokenScore; + } + + // Normalize by document length to prevent bias toward longer documents + // Use a gentle normalization: divide by log of token count + 1 + const lengthNorm = Math.log(1 + tokenized.tokens.length / 100); + if (lengthNorm > 1) { + score /= lengthNorm; + } + + return score; + } + + /** + * Check for exact or stemmed word match + * Handles plurals and common suffixes + */ + private exactMatch(textToken: string, queryToken: string): boolean { + // Exact match + if (textToken === queryToken) { + return true; + } + + // Handle plurals + if (queryToken.endsWith('s') && textToken === queryToken.slice(0, -1)) { + return true; + } + if (textToken.endsWith('s') && textToken.slice(0, -1) === queryToken) { + return true; + } + + // Handle -ed and -ing suffixes (simple stemmer) + const stem = (word: string): string => { + if (word.endsWith('ing')) return word.slice(0, -3); + if (word.endsWith('ed')) return word.slice(0, -2); + return word; + }; + + return stem(textToken) === stem(queryToken); + } +} diff --git a/tests/chat-view.test.ts b/tests/chat-view.test.ts new file mode 100644 index 0000000..5ad65fb --- /dev/null +++ b/tests/chat-view.test.ts @@ -0,0 +1,211 @@ +import { ChatView } from '../src/chat-view'; +import { PluginSettings, OllamaMessage, ChatMessage, OllamaTool, ToolCall } from '../src/types'; + +// Mock Obsidian types +interface MockVault { + getMarkdownFiles: () => any[]; + read: () => Promise; + create: () => Promise; +} +interface MockWorkspace { + getLeaf: () => any; + revealLeaf: () => void; +} +interface MockApp { + vault: MockVault; + workspace: MockWorkspace; +} + +// Mock Obsidian module - ItemView must set this.app from the leaf +jest.mock('obsidian', () => ({ + ItemView: jest.fn().mockImplementation(function (this: any, leaf: any) { + this.app = leaf.app; + }), + WorkspaceLeaf: jest.fn(), + Notice: jest.fn(), +})); + +const mockSettings: PluginSettings = { + ollamaUrl: 'http://localhost:11434', + model: 'llama3', + lastIndexTime: 0, +}; + +describe('ChatView', () => { + let view: ChatView; + let mockLeaf: any; + let mockApp: MockApp; + + beforeEach(() => { + mockApp = { + vault: { + getMarkdownFiles: jest.fn().mockReturnValue([]), + read: jest.fn().mockResolvedValue(''), + create: jest.fn().mockResolvedValue(null), + }, + workspace: { + getLeaf: jest.fn(), + revealLeaf: jest.fn(), + }, + }; + + mockLeaf = { + view: null, + setViewState: jest.fn(), + app: mockApp, + }; + + view = new ChatView(mockLeaf as unknown as any, mockSettings); + // Obsidian's contentEl has a createEl helper that standard DOM lacks + // Unlike standard DOM, Obsidian elements can create nested elements with createEl + const contentDiv = document.createElement('div') as any; + + // Create a factory function that captures the parent element + const createElementWithCreateEl = function (parent: any) { + return function (tag: string, options?: { cls?: string }) { + const el = document.createElement(tag); + if (options?.cls) { + el.classList.add(...options.cls.split(' ')); + } + parent.appendChild(el); + // Add createEl to the new element so it can create nested elements + (el as any).createEl = createElementWithCreateEl(el); + return el; + }; + }; + + contentDiv.createEl = createElementWithCreateEl(contentDiv); + view.contentEl = contentDiv; + }); + + describe('getViewType', () => { + it('should return the correct view type', () => { + expect(view.getViewType()).toBe('ollama-chat-view'); + }); + }); + + describe('getDisplayText', () => { + it('should return the correct display text', () => { + expect(view.getDisplayText()).toBe('Ollama Chat'); + }); + }); + + describe('onOpen', () => { + it('should call render and setup event listeners', async () => { + const renderSpy = jest.spyOn(view, 'render'); + const setupSpy = jest.spyOn(view, 'setupEventListeners' as any); + await view.onOpen(); + expect(renderSpy).toHaveBeenCalled(); + expect(setupSpy).toHaveBeenCalled(); + }); + }); + + describe('onClose', () => { + it('should clean up resources and remove event listeners', async () => { + view['lastMessageEl'] = document.createElement('div'); + view['sendButton'] = document.createElement('button'); + view['inputEl'] = document.createElement('textarea'); + const cancelSpy = jest.spyOn(view['ollamaClient'], 'cancelStream'); + const removeSpy = jest.spyOn(view, 'removeEventListeners' as any); + await view.onClose(); + expect(view['lastMessageEl']).toBeNull(); + expect(view['sendButton']).toBeNull(); + expect(view['inputEl']).toBeNull(); + expect(cancelSpy).toHaveBeenCalled(); + expect(removeSpy).toHaveBeenCalled(); + }); + }); + + describe('render', () => { + it('should render the chat interface', async () => { + await view.render(); + expect(view.contentEl.querySelector('.ollama-chat-container')).not.toBeNull(); + expect(view.contentEl.querySelector('.ollama-input-container')).not.toBeNull(); + }); + + it('should not duplicate elements on re-render', async () => { + // First render + await view.render(); + const firstRenderCount = view.contentEl.querySelectorAll('.ollama-message').length; + + // Second render with no changes + await view.render(); + const secondRenderCount = view.contentEl.querySelectorAll('.ollama-message').length; + + expect(secondRenderCount).toBe(firstRenderCount); + }); + + it('should only render non-streaming messages', async () => { + view['messages'] = [ + { id: '1', role: 'user', content: 'test', timestamp: Date.now() }, + { + id: '2', + role: 'assistant', + content: 'response', + timestamp: Date.now(), + isStreaming: true, + }, + ]; + await view.render(); + const messages = view.contentEl.querySelectorAll('.ollama-message'); + expect(messages.length).toBe(1); + }); + }); + + describe('handleUserInput', () => { + it('should handle user input and call ollamaClient', async () => { + view['sendButton'] = document.createElement('button'); + view['inputEl'] = document.createElement('textarea'); + (view['inputEl'] as HTMLTextAreaElement).value = 'test'; + const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockResolvedValue({ + [Symbol.asyncIterator]: async function* () { + yield { content: 'test' }; + }, + } as any); + await (view as any).handleUserInput('test'); + expect(chatSpy).toHaveBeenCalled(); + // Verify that messages were added to conversation history + expect((view as any).messages.length).toBeGreaterThan(0); + }); + + it('should process tool calls with follow-up context', async () => { + view['sendButton'] = document.createElement('button'); + view['inputEl'] = document.createElement('textarea'); + (view['inputEl'] as HTMLTextAreaElement).value = 'test'; + const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockResolvedValue({ + [Symbol.asyncIterator]: async function* () { + yield { + content: 'test', + tool_calls: [{ function: { name: 'create_file', arguments: '{}' } }], + }; + }, + } as any); + const followUpSpy = jest + .spyOn(view['ollamaClient'], 'chat') + .mockResolvedValue({ content: ' follow-up' }); + await (view as any).handleUserInput('test'); + expect(followUpSpy).toHaveBeenCalled(); + // Verify that tool calls resulted in follow-up messages + expect((view as any).messages.length).toBeGreaterThan(1); + }); + }); + + describe('event listeners', () => { + it('should setup event listeners on open', async () => { + view['sendButton'] = document.createElement('button'); + view['inputEl'] = document.createElement('textarea'); + await view.onOpen(); + expect(view['sendButtonClickHandler']).not.toBeNull(); + expect(view['inputKeyDownHandler']).not.toBeNull(); + }); + + it('should remove event listeners on close', async () => { + view['sendButton'] = document.createElement('button'); + view['inputEl'] = document.createElement('textarea'); + await view.onOpen(); + const removeSpy = jest.spyOn(view, 'removeEventListeners' as any); + await view.onClose(); + expect(removeSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/tests/error-handler.test.ts b/tests/error-handler.test.ts new file mode 100644 index 0000000..2abb359 --- /dev/null +++ b/tests/error-handler.test.ts @@ -0,0 +1,191 @@ +import { ErrorHandler } from '../src/error-handler'; +import { + OllamaError, + ErrorType, + NetworkError, + ApiError, + ValidationError, + StreamingError, + ToolExecutionError, + PathValidationError, +} from '../src/types'; + +// Mock Notice from Obsidian +jest.mock('obsidian', () => ({ + Notice: jest.fn(), +})); + +describe('ErrorHandler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('handleError', () => { + it('should handle OllamaError with user-friendly message', () => { + const error = new NetworkError('Connection failed'); + ErrorHandler.handleError(error); + expect(require('obsidian').Notice).toHaveBeenCalledWith( + 'Connection error. Please check if Ollama is running.' + ); + }); + + it('should handle generic Error with user-friendly message', () => { + const error = new Error('Network timeout occurred'); + ErrorHandler.handleError(error); + expect(require('obsidian').Notice).toHaveBeenCalledWith( + 'Request timed out. Please check your Ollama connection.' + ); + }); + + it('should handle unknown error type with generic message', () => { + const error = 'some string error'; + ErrorHandler.handleError(error); + expect(require('obsidian').Notice).toHaveBeenCalledWith('An unexpected error occurred'); + }); + + it('should log detailed error information to console', () => { + const error = new Error('Test error'); + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + ErrorHandler.handleError(error); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Test error')); + consoleSpy.mockRestore(); + }); + }); + + describe('NetworkError handling', () => { + it('should create user-friendly message for network errors', () => { + const error = new NetworkError('Connection failed'); + const message = ErrorHandler['getUserFriendlyMessage'](error); + expect(message).toBe('Connection error. Please check if Ollama is running.'); + }); + + it('should include status code in error details', () => { + const error = new NetworkError('Connection failed', 500); + expect(error.statusCode).toBe(500); + }); + }); + + describe('ValidationError handling', () => { + it('should create user-friendly message for validation errors', () => { + const error = new ValidationError('Invalid input', { + field: 'path', + message: 'Path is required', + }); + const message = ErrorHandler['getUserFriendlyMessage'](error); + expect(message).toContain('Invalid path'); + }); + + it('should handle validation errors without field details', () => { + const error = new ValidationError('Invalid input'); + const message = ErrorHandler['getUserFriendlyMessage'](error); + expect(message).toBe('Input validation error. Please correct your input.'); + }); + }); + + describe('StreamingError handling', () => { + it('should create user-friendly message for streaming errors', () => { + const error = new StreamingError('Response too long'); + const message = ErrorHandler['getUserFriendlyMessage'](error); + expect(message).toBe('Response too long. Please try a shorter request.'); + }); + }); + + describe('ToolExecutionError handling', () => { + it('should create user-friendly message for tool errors', () => { + const error = new ToolExecutionError('Tool failed', 'create_file'); + const message = ErrorHandler['getUserFriendlyMessage'](error); + expect(message).toContain('Tool error'); + expect(message).toContain('create_file'); + }); + }); + + describe('PathValidationError handling', () => { + it('should create user-friendly message for path errors', () => { + const error = new PathValidationError('Invalid path', '/../test.md'); + const message = ErrorHandler['getUserFriendlyMessage'](error); + expect(message).toContain('Invalid file path'); + }); + }); + + describe('Error creation methods', () => { + it('should create NetworkError with proper type', () => { + const error = ErrorHandler.createNetworkError('Connection failed'); + expect(error).toBeInstanceOf(NetworkError); + expect(error.type).toBe(ErrorType.NETWORK_ERROR); + }); + + it('should create ApiError with proper type', () => { + const error = ErrorHandler.createApiError('API error'); + expect(error).toBeInstanceOf(ApiError); + expect(error.type).toBe(ErrorType.API_ERROR); + }); + + it('should create ValidationError with proper type', () => { + const error = ErrorHandler.createValidationError('Validation failed', 'path'); + expect(error).toBeInstanceOf(ValidationError); + expect(error.type).toBe(ErrorType.VALIDATION_ERROR); + }); + + it('should create StreamingError with proper type', () => { + const error = ErrorHandler.createStreamingError('Streaming failed'); + expect(error).toBeInstanceOf(StreamingError); + expect(error.type).toBe(ErrorType.STREAMING_ERROR); + }); + + it('should create ToolExecutionError with proper type', () => { + const error = ErrorHandler.createToolExecutionError('Tool failed'); + expect(error).toBeInstanceOf(ToolExecutionError); + expect(error.type).toBe(ErrorType.TOOL_EXECUTION_ERROR); + }); + + it('should create PathValidationError with proper type', () => { + const error = ErrorHandler.createPathValidationError('Path invalid'); + expect(error).toBeInstanceOf(PathValidationError); + expect(error.type).toBe(ErrorType.PATH_VALIDATION_ERROR); + }); + + it('should create UnknownError with proper type', () => { + const error = ErrorHandler.createUnknownError('Unknown error'); + expect(error).toBeInstanceOf(OllamaError); + expect(error.type).toBe(ErrorType.UNKNOWN_ERROR); + }); + }); + + describe('Error message detection', () => { + it('should detect network-related errors in generic Error', () => { + const error = new Error('Network connection failed'); + const message = ErrorHandler['getUserFriendlyMessageFromError'](error); + expect(message).toContain('Connection error'); + }); + + it('should detect timeout errors in generic Error', () => { + const error = new Error('Request timeout occurred'); + const message = ErrorHandler['getUserFriendlyMessageFromError'](error); + expect(message).toContain('timed out'); + }); + + it('should detect validation errors in generic Error', () => { + const error = new Error('Validation format error'); + const message = ErrorHandler['getUserFriendlyMessageFromError'](error); + expect(message).toContain('Invalid input'); + }); + + it('should detect streaming errors in generic Error', () => { + const error = new Error('Streaming chunk error'); + const message = ErrorHandler['getUserFriendlyMessageFromError'](error); + expect(message).toContain('Response too long'); + }); + + it('should detect tool errors in generic Error', () => { + const error = new Error('Tool function error'); + const message = ErrorHandler['getUserFriendlyMessageFromError'](error); + expect(message).toContain('Tool execution error'); + }); + + it('should detect path errors in generic Error', () => { + const error = new Error('Path file error'); + const message = ErrorHandler['getUserFriendlyMessageFromError'](error); + expect(message).toContain('Invalid file path'); + }); + }); +}); diff --git a/tests/ollama-client.test.ts b/tests/ollama-client.test.ts new file mode 100644 index 0000000..59f8985 --- /dev/null +++ b/tests/ollama-client.test.ts @@ -0,0 +1,368 @@ +import { OllamaClient } from '../src/ollama-client'; +import { OllamaMessage, OllamaTool } from '../src/types'; + +describe('OllamaClient', () => { + let client: OllamaClient; + let mockFetch: jest.Mock; + + const mockMessages: OllamaMessage[] = [ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: 'Hello' }, + ]; + + const mockTools: OllamaTool[] = [ + { + type: 'function', + function: { + name: 'test_tool', + description: 'A test tool', + parameters: { + type: 'object', + properties: { input: { type: 'string' } }, + required: ['input'], + }, + }, + }, + ]; + + beforeEach(() => { + mockFetch = jest.fn(); + client = new OllamaClient('http://localhost:11434', 'llama3', mockFetch); + }); + + afterEach(() => { + jest.clearAllMocks(); + client.cancelStream(); + }); + + describe('chat (non-streaming)', () => { + it('should send a non-streaming request and return the response', async () => { + const mockResponse = { + ok: true, + json: () => Promise.resolve({ message: { content: 'Hello back!' } }), + }; + mockFetch.mockResolvedValue(mockResponse); + + const result = await client.chat(mockMessages, mockTools); + + expect(result.content).toBe('Hello back!'); + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:11434/api/chat', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ + model: 'llama3', + messages: mockMessages, + tools: mockTools, + stream: false, + }), + }) + ); + }); + + it('should throw on non-OK response', async () => { + mockFetch.mockResolvedValue({ ok: false, status: 500 }); + + await expect(client.chat(mockMessages, mockTools)).rejects.toThrow('Ollama API error: 500'); + }); + + it('should handle missing message content gracefully', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({}), + }); + + const result = await client.chat(mockMessages, mockTools); + expect(result.content).toBe(''); + expect(result.tool_calls).toEqual([]); + }); + + it('should include abort signal in fetch options', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ message: { content: 'ok' } }), + }); + + await client.chat(mockMessages, mockTools); + + const fetchOptions = mockFetch.mock.calls[0][1]; + expect(fetchOptions.signal).toBeInstanceOf(AbortSignal); + }); + + it('should forward tool_calls from response when present', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ + message: { + content: 'result', + tool_calls: [{ function: { name: 'create_file', arguments: '{}' } }], + }, + }), + }); + + const result = await client.chat(mockMessages, mockTools); + expect(result.tool_calls).toEqual([{ function: { name: 'create_file', arguments: '{}' } }]); + }); + }); + + describe('streamChat', () => { + function createMockReader(data: string) { + const encoder = new TextEncoder(); + const encoded = encoder.encode(data); + let called = false; + return { + read: () => { + if (!called) { + called = true; + return Promise.resolve({ done: false, value: encoded }); + } + return Promise.resolve({ done: true, value: new Uint8Array(0) }); + }, + releaseLock: jest.fn(), + }; + } + + it('should send a streaming request and yield chunks', async () => { + const streamData = [ + JSON.stringify({ message: { content: 'He' } }), + JSON.stringify({ message: { content: 'llo' } }), + JSON.stringify({ message: { content: '!' } }), + '', + ].join('\n'); + + const mockReader = createMockReader(streamData); + mockFetch.mockResolvedValue({ + ok: true, + body: { getReader: () => mockReader }, + headers: { + get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null), + }, + }); + + const stream = await client.streamChat(mockMessages, mockTools); + const chunks: string[] = []; + + for await (const chunk of stream) { + chunks.push(chunk.content); + } + + expect(chunks).toEqual(['He', 'llo', '!']); + expect(mockReader.releaseLock).toHaveBeenCalled(); + }); + + it('should skip malformed JSON chunks and log a warning', async () => { + const streamData = [ + JSON.stringify({ message: { content: 'valid' } }), + 'this is not json', + JSON.stringify({ message: { content: 'also valid' } }), + '', + ].join('\n'); + + const mockReader = createMockReader(streamData); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + mockFetch.mockResolvedValue({ + ok: true, + body: { getReader: () => mockReader }, + headers: { + get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null), + }, + }); + + const stream = await client.streamChat(mockMessages, mockTools); + const chunks: string[] = []; + + for await (const chunk of stream) { + chunks.push(chunk.content); + } + + expect(chunks).toEqual(['valid', 'also valid']); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('Skipped malformed chunk'), + expect.stringContaining('is not valid JSON') + ); + consoleWarnSpy.mockRestore(); + }); + + it('should throw when too many chunks are malformed', async () => { + const streamData = Array(51).fill('invalid json').join('\n') + '\n'; + + const mockReader = createMockReader(streamData); + + mockFetch.mockResolvedValue({ + ok: true, + body: { getReader: () => mockReader }, + headers: { + get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null), + }, + }); + + const stream = await client.streamChat(mockMessages, mockTools); + + await expect( + (async () => { + for await (const _ of stream) { + /* consume */ + } + })() + ).rejects.toThrow(/malformed/); + }); + + it('should throw on non-OK response', async () => { + mockFetch.mockResolvedValue({ ok: false, status: 404 }); + + await expect(client.streamChat(mockMessages, mockTools)).rejects.toThrow( + 'Ollama API error: 404' + ); + }); + + it('should throw when response has no body', async () => { + mockFetch.mockResolvedValue({ ok: true, body: undefined }); + + await expect(client.streamChat(mockMessages, mockTools)).rejects.toThrow('No response body'); + }); + + it('should throw on invalid content type', async () => { + mockFetch.mockResolvedValue({ + ok: true, + body: { + getReader: () => ({ + read: () => Promise.resolve({ done: true, value: new Uint8Array(0) }), + }), + }, + headers: { + get: (name: string) => (name === 'content-type' ? 'text/html' : null), + }, + }); + + await expect(client.streamChat(mockMessages, mockTools)).rejects.toThrow( + 'Invalid response format' + ); + }); + + it('should propagate Ollama error messages from the stream', async () => { + const streamData = JSON.stringify({ message: { error: 'model not found' } }) + '\n'; + + const mockReader = createMockReader(streamData); + + mockFetch.mockResolvedValue({ + ok: true, + body: { getReader: () => mockReader }, + headers: { + get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null), + }, + }); + + const stream = await client.streamChat(mockMessages, mockTools); + + await expect( + (async () => { + for await (const _ of stream) { + /* consume */ + } + })() + ).rejects.toThrow('Ollama error: model not found'); + }); + + it('should yield tool_calls when present in streamed response', async () => { + const streamData = [ + JSON.stringify({ + message: { + content: '', + tool_calls: [{ function: { name: 'create_file', arguments: '{"path":"a.md"}' } }], + }, + }), + '', + ].join('\n'); + + const mockReader = createMockReader(streamData); + + mockFetch.mockResolvedValue({ + ok: true, + body: { getReader: () => mockReader }, + headers: { + get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null), + }, + }); + + const stream = await client.streamChat(mockMessages, mockTools); + let lastChunk: any; + + for await (const chunk of stream) { + lastChunk = chunk; + } + + expect(lastChunk.tool_calls).toEqual([ + { function: { name: 'create_file', arguments: '{"path":"a.md"}' } }, + ]); + }); + + it('should default tool_calls to empty array when not present', async () => { + const streamData = JSON.stringify({ message: { content: 'hello' } }) + '\n'; + + const mockReader = createMockReader(streamData); + + mockFetch.mockResolvedValue({ + ok: true, + body: { getReader: () => mockReader }, + headers: { + get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null), + }, + }); + + const stream = await client.streamChat(mockMessages, mockTools); + let lastChunk: any; + + for await (const chunk of stream) { + lastChunk = chunk; + } + + expect(lastChunk.tool_calls).toEqual([]); + }); + + it('should send correct request body with stream:true', async () => { + const streamData = JSON.stringify({ message: { content: 'ok' } }) + '\n'; + const mockReader = createMockReader(streamData); + + mockFetch.mockResolvedValue({ + ok: true, + body: { getReader: () => mockReader }, + headers: { + get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null), + }, + }); + + const stream = await client.streamChat(mockMessages, mockTools); + for await (const _ of stream) { + /* consume */ + } + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:11434/api/chat', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ + model: 'llama3', + messages: mockMessages, + tools: mockTools, + stream: true, + }), + signal: expect.any(AbortSignal), + }) + ); + }); + }); + + describe('cancelStream', () => { + it('should abort the current request', () => { + client.cancelStream(); + expect(client['abortController']).toBeNull(); + }); + + it('should handle cancel when no active stream', () => { + expect(() => client.cancelStream()).not.toThrow(); + expect(client['abortController']).toBeNull(); + }); + }); +}); diff --git a/tests/tool-executor.test.ts b/tests/tool-executor.test.ts new file mode 100644 index 0000000..3e6d7f9 --- /dev/null +++ b/tests/tool-executor.test.ts @@ -0,0 +1,429 @@ +import { ToolExecutor } from '../src/tool-executor'; +import { ToolCall, ToolResult } from '../src/types'; +import { ErrorHandler } from '../src/error-handler'; + +// Mock Obsidian types +interface MockVault { + create: (path: string, content: string) => Promise; +} +interface MockApp { + // Mock app properties if needed +} +interface MockNotice { + (message: string): void; +} + +// Mock Obsidian module +jest.mock('obsidian', () => ({ + Vault: jest.fn(), + App: jest.fn(), + Notice: jest.fn(), +})); + +// Mock ErrorHandler +jest.mock('../src/error-handler', () => ({ + ErrorHandler: { + handleError: jest.fn(), + }, +})); + +describe('ToolExecutor', () => { + let executor: ToolExecutor; + let mockVault: MockVault; + let mockApp: MockApp; + + beforeEach(() => { + mockVault = { + create: jest.fn().mockResolvedValue(null), + }; + mockApp = {} as MockApp; + executor = new ToolExecutor(mockVault as unknown as any, mockApp as unknown as any); + jest.clearAllMocks(); + }); + + describe('handleToolCall', () => { + describe('create_file tool', () => { + it('should successfully create a file with valid arguments', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test-file.md', + content: 'Test content', + }), + }, + }; + const result = await executor.handleToolCall(call); + expect(result).toEqual({ success: true, message: 'File created successfully' }); + expect(mockVault.create).toHaveBeenCalledWith('test-file.md', 'Test content'); + }); + + it('should handle object arguments directly', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: { + path: 'obj-args-file.md', + content: 'Object args content', + } as unknown as string, + }, + }; + const result = await executor.handleToolCall(call); + expect(result).toEqual({ success: true, message: 'File created successfully' }); + expect(mockVault.create).toHaveBeenCalledWith('obj-args-file.md', 'Object args content'); + }); + + it('should successfully create a file in a subdirectory', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'subdirectory/test-file.md', + content: 'Subdir content', + }), + }, + }; + const result = await executor.handleToolCall(call); + expect(result).toEqual({ success: true, message: 'File created successfully' }); + expect(mockVault.create).toHaveBeenCalledWith( + 'subdirectory/test-file.md', + 'Subdir content' + ); + }); + + it('should handle multiple slashes gracefully by normalizing path', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test//file.md', + content: 'Test content', + }), + }, + }; + const result = await executor.handleToolCall(call); + expect(result).toEqual({ success: true, message: 'File created successfully' }); + expect(mockVault.create).toHaveBeenCalledWith('test//file.md', 'Test content'); + }); + + it('should handle empty content gracefully', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'empty-file.md', + content: '', + }), + }, + }; + const result = await executor.handleToolCall(call); + expect(result).toEqual({ success: true, message: 'File created successfully' }); + expect(mockVault.create).toHaveBeenCalledWith('empty-file.md', ''); + }); + + it('should allow filenames with consecutive dots', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'project..notes.md', + content: 'Test content', + }), + }, + }; + const result = await executor.handleToolCall(call); + expect(result).toEqual({ success: true, message: 'File created successfully' }); + expect(mockVault.create).toHaveBeenCalledWith('project..notes.md', 'Test content'); + }); + + it('should reject path traversal attempts with ..', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: '../test-file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path traversal attempts with .\\', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: '.\\test-file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path traversal attempts with /..', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: '/../test-file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject absolute paths starting with /', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: '/var/test-file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject absolute paths starting with \\', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: '\\var\\test-file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject Windows drive letters', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'C:\\test-file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject empty path', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: '', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject undefined path', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path with invalid characters <', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test>file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path with invalid characters :', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test:file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path with invalid characters |', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test|file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path with invalid characters ?', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test?file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path with invalid characters *', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test*file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path longer than 200 characters', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'a'.repeat(201) + '.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject path with ~ character', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test~file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject non-string content', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test-file.md', + content: 123, + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should reject non-string path', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 123, + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + + it('should handle vault.create rejection gracefully', async () => { + mockVault.create = jest.fn().mockRejectedValue(new Error('Permission denied')); + const call: ToolCall = { + function: { + name: 'create_file', + arguments: JSON.stringify({ + path: 'test-file.md', + content: 'Test content', + }), + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + }); + + it('should handle invalid JSON in arguments', async () => { + const call: ToolCall = { + function: { + name: 'create_file', + arguments: 'invalid json', + }, + }; + await expect(executor.handleToolCall(call)).rejects.toThrow(); + expect(mockVault.create).not.toHaveBeenCalled(); + }); + }); + + describe('unknown tool', () => { + it('should return failure for unknown tool', async () => { + const call: ToolCall = { + function: { + name: 'unknown_tool', + arguments: JSON.stringify({}), + }, + }; + const result = await executor.handleToolCall(call); + expect(result).toEqual({ success: false, message: 'Unknown tool: unknown_tool' }); + }); + }); + }); +}); diff --git a/tests/vault-indexer.test.ts b/tests/vault-indexer.test.ts new file mode 100644 index 0000000..203fe21 --- /dev/null +++ b/tests/vault-indexer.test.ts @@ -0,0 +1,399 @@ +import { VaultIndexer } from '../src/vault-indexer'; +import { VaultIndexEntry } from '../src/types'; + +// Mock Obsidian types +interface MockTFile { + basename: string; + path: string; +} + +interface MockVault { + getMarkdownFiles: () => MockTFile[]; + read: (file: MockTFile) => Promise; +} + +describe('VaultIndexer', () => { + let indexer: VaultIndexer; + let mockVault: MockVault; + + beforeEach(() => { + mockVault = { + getMarkdownFiles: jest.fn().mockReturnValue([]), + read: jest.fn(), + }; + indexer = new VaultIndexer(mockVault as unknown as any); + jest.clearAllMocks(); + }); + + describe('searchVault', () => { + it('should return empty array when no files exist', async () => { + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([]); + const results = await indexer.searchVault('test', 5); + expect(results).toEqual([]); + }); + + it('should return empty array for empty or whitespace-only query', async () => { + const file: MockTFile = { basename: 'test', path: 'test.md' }; + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file]); + mockVault.read = jest.fn().mockResolvedValue('some content'); + + const results1 = await indexer.searchVault('', 5); + const results2 = await indexer.searchVault(' ', 5); + expect(results1).toEqual([]); + expect(results2).toEqual([]); + }); + + it('should return files matching the query', async () => { + const file1: MockTFile = { basename: 'notes', path: 'notes.md' }; + const file2: MockTFile = { basename: 'todo', path: 'todo.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file1, file2]); + mockVault.read = jest.fn().mockImplementation(async (file: MockTFile) => { + if (file.basename === 'notes') { + return 'These are my important notes about programming algorithms'; + } + return 'Buy milk and eggs'; + }); + + const results = await indexer.searchVault('programming', 5); + expect(results.length).toBe(1); + expect(results[0].title).toBe('notes'); + expect(results[0].score).toBeGreaterThan(0); + }); + + it('should respect the limit parameter', async () => { + const files: MockTFile[] = []; + for (let i = 0; i < 10; i++) { + files.push({ basename: `file${i}`, path: `file${i}.md` }); + } + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue(files); + mockVault.read = jest.fn().mockResolvedValue('important keyword test'); + + const results = await indexer.searchVault('keyword', 3); + expect(results.length).toBeLessThanOrEqual(3); + }); + + it('should return results sorted by score descending', async () => { + const file1: MockTFile = { basename: 'one', path: 'one.md' }; + const file2: MockTFile = { basename: 'two', path: 'two.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file1, file2]); + mockVault.read = jest.fn().mockImplementation(async (file: MockTFile) => { + if (file.basename === 'one') { + return 'keyword keyword keyword important'; + } + return 'keyword'; + }); + + const results = await indexer.searchVault('keyword', 5); + if (results.length >= 2) { + expect(results[0].score).toBeGreaterThanOrEqual(results[1].score); + } + }); + + it('should truncate content previews to 500 characters', async () => { + const file: MockTFile = { basename: 'long', path: 'long.md' }; + const longContent = 'content '.repeat(100); // Use meaningful words, not just 'a' + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file]); + mockVault.read = jest.fn().mockResolvedValue(longContent); + + const results = await indexer.searchVault('content', 5); + expect(results.length).toBeGreaterThan(0); + expect(results[0].content.length).toBeLessThanOrEqual(500); + }); + + it('should process files in batches to handle large vaults', async () => { + const files: MockTFile[] = []; + for (let i = 0; i < 25; i++) { + files.push({ basename: `file${i}`, path: `file${i}.md` }); + } + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue(files); + mockVault.read = jest.fn().mockResolvedValue('important test keyword'); + + const results = await indexer.searchVault('test', 5); + expect(mockVault.read).toHaveBeenCalledTimes(25); + expect(results.length).toBeGreaterThan(0); + }); + + it('should filter out files with zero score', async () => { + const file1: MockTFile = { basename: 'match', path: 'match.md' }; + const file2: MockTFile = { basename: 'nomatch', path: 'nomatch.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file1, file2]); + mockVault.read = jest.fn().mockImplementation(async (file: MockTFile) => { + if (file.basename === 'match') { + return 'relevant keyword algorithm'; + } + return 'nothing relevant here at all'; + }); + + const results = await indexer.searchVault('keyword', 5); + expect(results.length).toBe(1); + expect(results[0].title).toBe('match'); + }); + + it('should handle vault.read errors gracefully', async () => { + const file1: MockTFile = { basename: 'good', path: 'good.md' }; + const file2: MockTFile = { basename: 'bad', path: 'bad.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file1, file2]); + mockVault.read = jest.fn().mockImplementation(async (file: MockTFile) => { + if (file.basename === 'good') { + return 'important keyword test'; + } + throw new Error('Permission denied'); + }); + + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const results = await indexer.searchVault('keyword', 5); + expect(results.length).toBe(1); + expect(results[0].title).toBe('good'); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Permission denied')); + + consoleWarnSpy.mockRestore(); + }); + + it('should give higher scores to title matches', async () => { + const file1: MockTFile = { basename: 'algorithm', path: 'algorithm.md' }; + const file2: MockTFile = { basename: 'other', path: 'other.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file1, file2]); + mockVault.read = jest.fn().mockImplementation(async (file: MockTFile) => { + if (file.basename === 'algorithm') { + return 'Some other content here'; + } + return 'This file discusses algorithm design patterns'; + }); + + const results = await indexer.searchVault('algorithm', 5); + expect(results.length).toBe(2); + // File with title match should be first + expect(results[0].title).toBe('algorithm'); + }); + + it('should give higher scores to heading matches', async () => { + const file1: MockTFile = { basename: 'file1', path: 'file1.md' }; + const file2: MockTFile = { basename: 'file2', path: 'file2.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file1, file2]); + mockVault.read = jest.fn().mockImplementation(async (file: MockTFile) => { + if (file.basename === 'file1') { + return '# Algorithm Design\n\nThis discusses design patterns'; + } + return 'This file mentions algorithm somewhere in the body text'; + }); + + const results = await indexer.searchVault('algorithm', 5); + expect(results.length).toBe(2); + // File with heading match should score higher + expect(results[0].title).toBe('file1'); + }); + + it('should give higher scores to frontmatter matches', async () => { + const file1: MockTFile = { basename: 'file1', path: 'file1.md' }; + const file2: MockTFile = { basename: 'file2', path: 'file2.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file1, file2]); + mockVault.read = jest.fn().mockImplementation(async (file: MockTFile) => { + if (file.basename === 'file1') { + return '---\ntags: algorithm design\n---\n\nSome content here'; + } + return 'This file mentions algorithm in the body'; + }); + + const results = await indexer.searchVault('algorithm', 5); + expect(results.length).toBe(2); + // File with frontmatter match should score higher + expect(results[0].title).toBe('file1'); + }); + + it('should handle phrase matching with bonus', async () => { + const file1: MockTFile = { basename: 'file1', path: 'file1.md' }; + const file2: MockTFile = { basename: 'file2', path: 'file2.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file1, file2]); + mockVault.read = jest.fn().mockImplementation(async (file: MockTFile) => { + if (file.basename === 'file1') { + return 'This discusses the design pattern algorithm'; + } + return 'This discusses design and pattern and algorithm separately'; + }); + + const results = await indexer.searchVault('design pattern', 5); + expect(results.length).toBe(2); + }); + + it('should filter out stop words from query', async () => { + const file: MockTFile = { basename: 'test', path: 'test.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file]); + mockVault.read = jest.fn().mockResolvedValue('important keyword here'); + + // Query with stop words should still find the keyword + const results = await indexer.searchVault('the important keyword', 5); + expect(results.length).toBe(1); + expect(results[0].title).toBe('test'); + }); + + it('should handle files with no matching content', async () => { + const file: MockTFile = { basename: 'test', path: 'test.md' }; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([file]); + mockVault.read = jest.fn().mockResolvedValue('nothing relevant'); + + const results = await indexer.searchVault('nonexistent', 5); + expect(results.length).toBe(0); + }); + }); + + describe('tokenize', () => { + it('should filter out stop words', () => { + const tokens = (indexer as any).tokenize('the quick brown fox'); + expect(tokens).not.toContain('the'); + expect(tokens).toContain('quick'); + expect(tokens).toContain('brown'); + expect(tokens).toContain('fox'); + }); + + it('should convert to lowercase', () => { + const tokens = (indexer as any).tokenize('Hello WORLD'); + expect(tokens).toEqual(['hello', 'world']); + }); + + it('should handle punctuation', () => { + const tokens = (indexer as any).tokenize('Hello, world!'); + expect(tokens).toEqual(['hello', 'world']); + }); + + it('should filter very short tokens', () => { + const tokens = (indexer as any).tokenize('a b test word'); + expect(tokens).not.toContain('a'); + expect(tokens).not.toContain('b'); + expect(tokens).toContain('test'); + expect(tokens).toContain('word'); + }); + }); + + describe('calculateWeightedScore', () => { + it('should return 0 when no tokens match', () => { + const content = 'important algorithm design'; + const queryTokens = (indexer as any).tokenize('nonexistent'); + const tokenized = (indexer as any).tokenizeContent(content, { + basename: 'test', + path: 'test.md', + } as any); + const score = (indexer as any).calculateWeightedScore(tokenized, '', queryTokens); + expect(score).toBe(0); + }); + + it('should score higher when more tokens match', () => { + const content = 'algorithm design pattern implementation'; + const tokenized = (indexer as any).tokenizeContent(content, { + basename: 'test', + path: 'test.md', + } as any); + const query1 = 'algorithm'; + const query2 = 'algorithm design pattern'; + const score1 = (indexer as any).calculateWeightedScore( + tokenized, + query1, + (indexer as any).tokenize(query1) + ); + const score2 = (indexer as any).calculateWeightedScore( + tokenized, + query2, + (indexer as any).tokenize(query2) + ); + expect(score2).toBeGreaterThan(score1); + }); + + it('should be case insensitive', () => { + const content = 'Important Algorithm Design'; + const tokenized = (indexer as any).tokenizeContent(content, { + basename: 'test', + path: 'test.md', + } as any); + const query = 'important algorithm'; + const score = (indexer as any).calculateWeightedScore( + tokenized, + query, + (indexer as any).tokenize(query) + ); + expect(score).toBeGreaterThan(0); + }); + + it('should handle word boundary matching', () => { + const content = 'algorithm'; + const tokenized = (indexer as any).tokenizeContent(content, { + basename: 'test', + path: 'test.md', + } as any); + const query = 'algorithm'; + const score = (indexer as any).calculateWeightedScore( + tokenized, + query, + (indexer as any).tokenize(query) + ); + expect(score).toBeGreaterThan(0); + }); + }); + + describe('exactMatch with stemming', () => { + it('should match exact words', () => { + expect((indexer as any).exactMatch('test', 'test')).toBe(true); + }); + + it('should match plurals', () => { + expect((indexer as any).exactMatch('tests', 'test')).toBe(true); + expect((indexer as any).exactMatch('test', 'tests')).toBe(true); + }); + + it('should handle -ed suffix', () => { + expect((indexer as any).exactMatch('tested', 'test')).toBe(true); + }); + + it('should handle -ing suffix', () => { + expect((indexer as any).exactMatch('testing', 'test')).toBe(true); + }); + + it('should not match unrelated words', () => { + expect((indexer as any).exactMatch('apple', 'banana')).toBe(false); + }); + }); + + describe('tokenizeContent', () => { + it('should extract headings from markdown', () => { + const content = '# Heading 1\n\n# Heading 2\n\nSome content'; + const tokenized = (indexer as any).tokenizeContent(content, { + basename: 'test', + path: 'test.md', + } as any); + expect(tokenized.headings).toContain('Heading 1'); + expect(tokenized.headings).toContain('Heading 2'); + }); + + it('should extract frontmatter', () => { + const content = '---\ntags: algorithm\ntitle: test\n---\n\nSome content'; + const tokenized = (indexer as any).tokenizeContent(content, { + basename: 'test', + path: 'test.md', + } as any); + expect(tokenized.frontmatter.tags).toBe('algorithm'); + expect(tokenized.frontmatter.title).toBe('test'); + }); + + it('should extract first paragraph', () => { + const content = 'First paragraph here.\n\nSecond paragraph here.'; + const tokenized = (indexer as any).tokenizeContent(content, { + basename: 'test', + path: 'test.md', + } as any); + expect(tokenized.firstParagraph).toContain('First'); + expect(tokenized.firstParagraph).not.toContain('Second'); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..86944cd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ESNext", "DOM"], + "module": "commonjs", + "outDir": "./lib", + "rootDir": ".", + "strict": true, + "types": ["node", "jest"], + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ESNext"], + "moduleResolution": "node", + "types": ["node", "jest"] + }, + "include": ["src/**/*", "main.ts"], + "typeRoots": ["node_modules/@types", "./src"], + "exclude": ["node_modules"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..794b757 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "types": ["node", "jest", "jsdom"] + }, + "include": ["tests/**/*"], + "exclude": ["node_modules"] +}