diff --git a/docs/superpowers/specs/2026-04-10-obsidian-rag-task-list.md b/docs/superpowers/specs/2026-04-10-obsidian-rag-task-list.md index 54b7feb..8e887eb 100644 --- a/docs/superpowers/specs/2026-04-10-obsidian-rag-task-list.md +++ b/docs/superpowers/specs/2026-04-10-obsidian-rag-task-list.md @@ -2,7 +2,7 @@ **Date:** 2026-04-10 **Based on:** Work Breakdown Structure v1.0 -**Last Updated:** 2026-04-10 +**Last Updated:** 2026-04-10 21:30 ## Legend - `[ ]` = Pending @@ -15,57 +15,57 @@ ## Phase 0: Project Scaffolding & Environment ### 0.1 Repository & Build Setup -- [ ] **0.1.1** Initialize TypeScript project structure (S) - Create package.json, tsconfig.json, src/ directory -- [ ] **0.1.2** Initialize Python package structure (S) - Create pyproject.toml, obsidian_rag/ module skeleton -- [ ] **0.1.3** Create development config file (S) - Depends on 0.1.1 - Create ./obsidian-rag/config.json -- [ ] **0.1.4** Set up OpenClaw plugin manifest (S) - Depends on 0.1.1 - Create openclaw.plugin.json -- [ ] **0.1.5** Configure test runners (S) - Depends on 0.1.1, 0.1.2 - Setup vitest and pytest configs +- [x] **0.1.1** Initialize TypeScript project structure (S) - Create package.json, tsconfig.json, src/ directory +- [x] **0.1.2** Initialize Python package structure (S) - Create pyproject.toml, obsidian_rag/ module skeleton +- [x] **0.1.3** Create development config file (S) - Depends on 0.1.1 - Create ./obsidian-rag/config.json +- [x] **0.1.4** Set up OpenClaw plugin manifest (S) - Depends on 0.1.1 - Create openclaw.plugin.json +- [x] **0.1.5** Configure test runners (S) - Depends on 0.1.1, 0.1.2 - Setup vitest and pytest configs ### 0.2 Environment Validation -- [ ] **0.2.1** Verify Ollama + mxbai-embed-large (S) - Test embedding API -- [ ] **0.2.2** Verify LanceDB Python package (S) - Test table creation and queries -- [ ] **0.2.3** Verify sample vault accessibility (S) - Count .md files in KnowledgeVault +- [x] **0.2.1** Verify Ollama + mxbai-embed-large (S) - Test embedding API +- [x] **0.2.2** Verify LanceDB Python package (S) - Test table creation and queries +- [x] **0.2.3** Verify sample vault accessibility (S) - Count .md files in KnowledgeVault --- ## Phase 1: Data Layer (Python Indexer) ### 1.1 Configuration (Python) -- [ ] **1.1.1** Implement config loader (S) - Depends on 0.1.2 - Read JSON, resolve paths, validate schema +- [x] **1.1.1** Implement config loader (S) - Depends on 0.1.2 - Read JSON, resolve paths, validate schema - [ ] **1.1.2** Write config tests (S) - Depends on 1.1.1 - Test validation and path resolution ### 1.2 Security (Python) - Can start after 1.1.1, parallel with other components -- [ ] **1.2.1** Implement path traversal prevention (S) - Depends on 1.1.1 - Validate paths, reject ../ and symlinks -- [ ] **1.2.2** Implement input sanitization (S) - Depends on 1.1.1 - Strip HTML, normalize whitespace -- [ ] **1.2.3** Implement sensitive content detection (S) - Depends on 1.1.1 - Detect health/financial/relations content -- [ ] **1.2.4** Implement directory access control (S) - Depends on 1.1.1 - Apply deny/allow lists -- [ ] **1.2.5** Write security tests (M) - Depends on 1.2.1-1.2.4 - Test all security functions +- [x] **1.2.1** Implement path traversal prevention (S) - Depends on 1.1.1 - Validate paths, reject ../ and symlinks +- [x] **1.2.2** Implement input sanitization (S) - Depends on 1.1.1 - Strip HTML, normalize whitespace +- [x] **1.2.3** Implement sensitive content detection (S) - Depends on 1.1.1 - Detect health/financial/relations content +- [x] **1.2.4** Implement directory access control (S) - Depends on 1.1.1 - Apply deny/allow lists +- [x] **1.2.5** Write security tests (M) - Depends on 1.2.1-1.2.4 - Test all security functions ### 1.3 Chunking - Can start after 1.1.1, parallel with security -- [ ] **1.3.1** Implement markdown parser (S) - Depends on 0.1.2 - Parse frontmatter, headings, tags -- [ ] **1.3.2** Implement structured chunker (M) - Depends on 1.3.1 - Split by section headers -- [ ] **1.3.3** Implement sliding window chunker (S) - Depends on 1.3.1 - 500 token window with overlap -- [ ] **1.3.4** Implement chunk router (S) - Depends on 1.3.2, 1.3.3 - Route structured vs unstructured -- [ ] **1.3.5** Write chunker tests (M) - Depends on 1.3.4 - Test all chunking scenarios +- [x] **1.3.1** Implement markdown parser (S) - Depends on 0.1.2 - Parse frontmatter, headings, tags +- [x] **1.3.2** Implement structured chunker (M) - Depends on 1.3.1 - Split by section headers +- [x] **1.3.3** Implement sliding window chunker (S) - Depends on 1.3.1 - 500 token window with overlap +- [x] **1.3.4** Implement chunk router (S) - Depends on 1.3.2, 1.3.3 - Route structured vs unstructured +- [x] **1.3.5** Write chunker tests (M) - Depends on 1.3.4 - Test all chunking scenarios ### 1.4 Embedding - Can start after 1.1.1, parallel with chunking/security -- [ ] **1.4.1** Implement Ollama embedder (M) - Depends on 1.1.1 - Batch 64 chunks, error handling +- [x] **1.4.1** Implement Ollama embedder (M) - Depends on 1.1.1 - Batch 64 chunks, error handling - [ ] **1.4.2** Implement embedding cache (S) - Depends on 1.4.1 - File-based cache - [ ] **1.4.3** Write embedder tests (S) - Depends on 1.4.1, 1.4.2 - Test batching and cache ### 1.5 Vector Store - Can start after 0.2.2, parallel with other components -- [ ] **1.5.1** Implement LanceDB table creation (S) - Depends on 0.2.2 - Create obsidian_chunks table -- [ ] **1.5.2** Implement vector upsert (S) - Depends on 1.5.1 - Add/update chunks -- [ ] **1.5.3** Implement vector delete (S) - Depends on 1.5.1 - Remove by source_file -- [ ] **1.5.4** Implement vector search (M) - Depends on 1.5.1 - Query with filters -- [ ] **1.5.5** Write vector store tests (M) - Depends on 1.5.2-1.5.4 - Test CRUD operations +- [x] **1.5.1** Implement LanceDB table creation (S) - Depends on 0.2.2 - Create obsidian_chunks table +- [x] **1.5.2** Implement vector upsert (S) - Depends on 1.5.1 - Add/update chunks +- [x] **1.5.3** Implement vector delete (S) - Depends on 1.5.1 - Remove by source_file +- [x] **1.5.4** Implement vector search (M) - Depends on 1.5.1 - Query with filters +- [x] **1.5.5** Write vector store tests (M) - Depends on 1.5.2-1.5.4 - Test CRUD operations ### 1.6 Indexer Pipeline & CLI - Depends on multiple components -- [ ] **1.6.1** Implement full index pipeline (M) - Depends on 1.2.4, 1.3.4, 1.4.1, 1.5.2 - Scan → parse → chunk → embed → store -- [ ] **1.6.2** Implement incremental sync (M) - Depends on 1.6.1, 1.5.3 - Compare mtime, process changes -- [ ] **1.6.3** Implement reindex (S) - Depends on 1.6.1 - Drop table + rebuild -- [ ] **1.6.4** Implement sync-result.json writer (S) - Depends on 1.6.1 - Atomic file writing -- [ ] **1.6.5** Implement CLI entry point (M) - Depends on 1.6.1, 1.6.2, 1.6.3 - index/sync/reindex commands +- [x] **1.6.1** Implement full index pipeline (M) - Depends on 1.2.4, 1.3.4, 1.4.1, 1.5.2 - Scan → parse → chunk → embed → store +- [x] **1.6.2** Implement incremental sync (M) - Depends on 1.6.1, 1.5.3 - Compare mtime, process changes +- [x] **1.6.3** Implement reindex (S) - Depends on 1.6.1 - Drop table + rebuild +- [x] **1.6.4** Implement sync-result.json writer (S) - Depends on 1.6.1 - Atomic file writing +- [x] **1.6.5** Implement CLI entry point (M) - Depends on 1.6.1, 1.6.2, 1.6.3 - index/sync/reindex commands - [ ] **1.6.6** Write indexer tests (M) - Depends on 1.6.5 - Test full pipeline and CLI --- @@ -73,40 +73,40 @@ ## Phase 2: Data Layer (TypeScript Client) ### 2.1 Configuration (TypeScript) - Can start after 0.1.1, parallel with Phase 1 -- [ ] **2.1.1** Implement config loader (S) - Depends on 0.1.1 - Read JSON, validate schema -- [ ] **2.1.2** Implement config types (S) - Depends on 2.1.1 - TypeScript interfaces +- [x] **2.1.1** Implement config loader (S) - Depends on 0.1.1 - Read JSON, validate schema +- [x] **2.1.2** Implement config types (S) - Depends on 2.1.1 - TypeScript interfaces ### 2.2 LanceDB Client - Depends on Phase 1 completion -- [ ] **2.2.1** Implement LanceDB query client (M) - Depends on 0.1.1 - Connect and search -- [ ] **2.2.2** Implement full-text search fallback (S) - Depends on 2.2.1 - Degraded mode +- [x] **2.2.1** Implement LanceDB query client (M) - Depends on 0.1.1 - Connect and search +- [~] **2.2.2** Implement full-text search fallback (S) - Depends on 2.2.1 - Degraded mode ### 2.3 Indexer Bridge - Depends on Phase 1 completion -- [ ] **2.3.1** Implement subprocess spawner (M) - Depends on 0.1.1 - Spawn Python CLI -- [ ] **2.3.2** Implement sync-result reader (S) - Depends on 2.3.1 - Read sync results -- [ ] **2.3.3** Implement job tracking (S) - Depends on 2.3.1 - Track progress +- [x] **2.3.1** Implement subprocess spawner (M) - Depends on 0.1.1 - Spawn Python CLI +- [x] **2.3.2** Implement sync-result reader (S) - Depends on 2.3.1 - Read sync results +- [x] **2.3.3** Implement job tracking (S) - Depends on 2.3.1 - Track progress --- ## Phase 3: Session & Transport Layers ### 3.1 Health State Machine - Depends on Phase 2 -- [ ] **3.1.1** Implement health prober (S) - Depends on 2.1.1, 2.2.1 - Probe dependencies -- [ ] **3.1.2** Implement state machine (S) - Depends on 3.1.1 - HEALTHY/DEGRADED/UNAVAILABLE -- [ ] **3.1.3** Implement staleness detector (S) - Depends on 3.1.2, 2.3.2 - Detect stale syncs +- [x] **3.1.1** Implement health prober (S) - Depends on 2.1.1, 2.2.1 - Probe dependencies +- [x] **3.1.2** Implement state machine (S) - Depends on 3.1.1 - HEALTHY/DEGRADED/UNAVAILABLE +- [x] **3.1.3** Implement staleness detector (S) - Depends on 3.1.2, 2.3.2 - Detect stale syncs ### 3.2 Vault Watcher - Depends on Phase 2 -- [ ] **3.2.1** Implement file watcher (S) - Depends on 2.1.1 - Watch vault directory -- [ ] **3.2.2** Implement debounce & batching (M) - Depends on 3.2.1 - Batch changes -- [ ] **3.2.3** Implement auto-sync trigger (M) - Depends on 3.2.2, 2.3.1, 3.1.2 - Trigger sync +- [x] **3.2.1** Implement file watcher (S) - Depends on 2.1.1 - Watch vault directory +- [x] **3.2.2** Implement debounce & batching (M) - Depends on 3.2.1 - Batch changes +- [x] **3.2.3** Implement auto-sync trigger (M) - Depends on 3.2.2, 2.3.1, 3.1.2 - Trigger sync - [ ] **3.2.4** Write vault watcher tests (M) - Depends on 3.2.3 - Test watcher behavior ### 3.3 Response Envelope & Error Normalization - Can start after 0.1.1, parallel -- [ ] **3.3.1** Implement response envelope factory (S) - Depends on 0.1.1 - Build response structure -- [ ] **3.3.2** Implement error normalizer (S) - Depends on 3.3.1 - Map exceptions to codes +- [x] **3.3.1** Implement response envelope factory (S) - Depends on 0.1.1 - Build response structure +- [x] **3.3.2** Implement error normalizer (S) - Depends on 3.3.1 - Map exceptions to codes ### 3.4 Security Guard (TypeScript) - Can start after 2.1.1, parallel with 3.1-3.2 -- [ ] **3.4.1** Implement directory filter validator (S) - Depends on 2.1.1 - Validate filters -- [ ] **3.4.2** Implement sensitive content flag (S) - Depends on 3.4.1 - Flag sensitive content +- [x] **3.4.1** Implement directory filter validator (S) - Depends on 2.1.1 - Validate filters +- [x] **3.4.2** Implement sensitive content flag (S) - Depends on 3.4.1 - Flag sensitive content - [ ] **3.4.3** Write security guard tests (S) - Depends on 3.4.2 - Test security functions --- @@ -114,14 +114,14 @@ ## Phase 4: Tool Layer ### 4.1 Tool Implementations - Depends on Phase 3 -- [ ] **4.1.1** Implement obsidian_rag_search tool (M) - Depends on 2.2.1, 3.3.1, 3.4.2 - Search with filters -- [ ] **4.1.2** Implement obsidian_rag_index tool (M) - Depends on 2.3.1, 2.3.3, 3.3.1 - Spawn indexer -- [ ] **4.1.3** Implement obsidian_rag_status tool (S) - Depends on 3.1.2, 2.3.2, 3.3.1 - Return health status -- [ ] **4.1.4** Implement obsidian_rag_memory_store tool (S) - Depends on 3.3.1 - Persist to memory +- [~] **4.1.1** Implement obsidian_rag_search tool (M) - Depends on 2.2.1, 3.3.1, 3.4.2 - Search with filters ⚠️ LanceDB TS client now wired, needs OpenClaw integration +- [~] **4.1.2** Implement obsidian_rag_index tool (M) - Depends on 2.3.1, 2.3.3, 3.3.1 - Spawn indexer ⚠️ stub — tool registration not wired to OpenClaw +- [~] **4.1.3** Implement obsidian_rag_status tool (S) - Depends on 3.1.2, 2.3.2, 3.3.1 - Return health status ⚠️ stub — reads sync-result not LanceDB stats +- [~] **4.1.4** Implement obsidian_rag_memory_store tool (S) - Depends on 3.3.1 - Persist to memory ⚠️ stub — no-op - [ ] **4.1.5** Write tool unit tests (M) - Depends on 4.1.1-4.1.4 - Test all tools ### 4.2 Plugin Registration - Depends on tools -- [ ] **4.2.1** Implement plugin entry point (M) - Depends on 4.1.1-4.1.4, 3.2.3, 3.1.2 - Plugin lifecycle +- [~] **4.2.1** Implement plugin entry point (M) - Depends on 4.1.1-4.1.4, 3.2.3, 3.1.2 - Plugin lifecycle ⚠️ stub — tools registration is a TODO - [ ] **4.2.2** Verify OpenClaw plugin lifecycle (S) - Depends on 4.2.1 - Manual test --- @@ -152,13 +152,13 @@ | Phase | Tasks | Done | Pending | In Progress | Blocked | |-------|-------|------|---------|-------------|---------| -| Phase 0: Scaffolding | 8 | 0 | 8 | 0 | 0 | -| Phase 1: Python Indexer | 20 | 0 | 20 | 0 | 0 | -| Phase 2: TS Client | 7 | 0 | 7 | 0 | 0 | -| Phase 3: Session/Transport | 10 | 0 | 10 | 0 | 0 | -| Phase 4: Tool Layer | 7 | 0 | 7 | 0 | 0 | +| Phase 0: Scaffolding | 8 | 8 | 0 | 0 | 0 | +| Phase 1: Python Indexer | 20 | 16 | 2 | 2 | 0 | +| Phase 2: TS Client | 7 | 6 | 0 | 1 | 0 | +| Phase 3: Session/Transport | 10 | 8 | 1 | 1 | 0 | +| Phase 4: Tool Layer | 7 | 1 | 5 | 1 | 0 | | Phase 5: Integration | 12 | 0 | 12 | 0 | 0 | -| **Total** | **64** | **0** | **64** | **0** | **0** | +| **Total** | **64** | **40** | **20** | **5** | **0** | --- diff --git a/openclaw.plugin.json b/openclaw.plugin.json new file mode 100644 index 0000000..271c8d3 --- /dev/null +++ b/openclaw.plugin.json @@ -0,0 +1,97 @@ +{ + "schema_version": "1.0", + "name": "obsidian-rag", + "version": "0.1.0", + "description": "Semantic search through Obsidian vault notes using RAG. Powers natural language queries like 'How was my mental health in 2024?' across journal entries, financial records, health data, and more.", + "author": "Santhosh Janardhanan", + "tools": [ + { + "name": "obsidian_rag_search", + "description": "Primary semantic search tool. Given a natural language query, searches the Obsidian vault index and returns the most relevant note chunks ranked by semantic similarity. Supports filtering by directory, date range, and tags.", + "parameter_schema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Natural language question or topic to search for" + }, + "max_results": { + "type": "integer", + "description": "Maximum number of chunks to return", + "default": 5, + "minimum": 1, + "maximum": 50 + }, + "directory_filter": { + "type": "array", + "description": "Limit search to specific vault subdirectories (e.g. ['Journal', 'Finance'])", + "items": { "type": "string" } + }, + "date_range": { + "type": "object", + "description": "Filter by date range", + "properties": { + "from": { "type": "string", "description": "Start date (YYYY-MM-DD)" }, + "to": { "type": "string", "description": "End date (YYYY-MM-DD)" } + } + }, + "tags": { + "type": "array", + "description": "Filter by hashtags found in notes (e.g. ['#mentalhealth', '#therapy'])", + "items": { "type": "string" } + } + }, + "required": ["query"] + }, + "required_permissions": [] + }, + { + "name": "obsidian_rag_index", + "description": "Trigger indexing of the Obsidian vault. Use 'full' for first-time setup, 'sync' for incremental updates, 'reindex' to force a clean rebuild.", + "parameter_schema": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "description": "Indexing mode", + "enum": ["full", "sync", "reindex"] + } + }, + "required": ["mode"] + }, + "required_permissions": [] + }, + { + "name": "obsidian_rag_status", + "description": "Check the health of the Obsidian RAG plugin — index statistics, last sync time, unindexed files, and Ollama status. Call this first when unsure if the index is ready.", + "parameter_schema": { + "type": "object", + "properties": {} + }, + "required_permissions": [] + }, + { + "name": "obsidian_rag_memory_store", + "description": "Commit an important fact from search results to OpenClaw's memory for faster future retrieval. Use after finding significant information (e.g. 'I owe Sreenivas $50') that should be remembered.", + "parameter_schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Identifier for the fact (e.g. 'debt_to_sreenivas')" + }, + "value": { + "type": "string", + "description": "The fact to remember" + }, + "source": { + "type": "string", + "description": "Source file path in the vault (e.g. 'Journal/2025-03-15.md')" + } + }, + "required": ["key", "value", "source"] + }, + "required_permissions": [] + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..179af63 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2513 @@ +{ + "name": "obsidian-rag", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "obsidian-rag", + "version": "0.1.0", + "dependencies": { + "@lancedb/lancedb": "^0.12", + "chokidar": "^3.6", + "yaml": "^2.5" + }, + "devDependencies": { + "@types/node": "^20.14", + "esbuild": "^0.24", + "typescript": "^5.5", + "vitest": "^2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lancedb/lancedb": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@lancedb/lancedb/-/lancedb-0.12.0.tgz", + "integrity": "sha512-YkfJ0pL0GT6A+goDqVsd/8jYeOJmPvT0A0aWpUbP/hgNn98FKJZicQKUHW6gwWyoZBVKTF9yBNKQS59aZOT+cg==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache 2.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "reflect-metadata": "^0.2.2" + }, + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "@lancedb/lancedb-darwin-arm64": "0.12.0", + "@lancedb/lancedb-darwin-x64": "0.12.0", + "@lancedb/lancedb-linux-arm64-gnu": "0.12.0", + "@lancedb/lancedb-linux-x64-gnu": "0.12.0", + "@lancedb/lancedb-win32-x64-msvc": "0.12.0" + }, + "peerDependencies": { + "apache-arrow": ">=13.0.0 <=17.0.0" + } + }, + "node_modules/@lancedb/lancedb-darwin-arm64": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@lancedb/lancedb-darwin-arm64/-/lancedb-darwin-arm64-0.12.0.tgz", + "integrity": "sha512-yuQkxgdR7q8eXeQ+8wOupB2789f0gS5+uvzZRiKz3ilf1ZgNTV68Zd3vgGjTTepcYGZvFvVOVlszlhZhVQjlfw==", + "cpu": [ + "arm64" + ], + "license": "Apache 2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 18" + } + }, + "node_modules/@lancedb/lancedb-darwin-x64": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@lancedb/lancedb-darwin-x64/-/lancedb-darwin-x64-0.12.0.tgz", + "integrity": "sha512-ZOVVDJRaEch/54zbDSVRbFXZRCgOEYRaqrcIUSZMKrMgFhinq5xgrau4zLGRsF7rSrxeCoF6eMx9+qkQHotyig==", + "cpu": [ + "x64" + ], + "license": "Apache 2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 18" + } + }, + "node_modules/@lancedb/lancedb-linux-arm64-gnu": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@lancedb/lancedb-linux-arm64-gnu/-/lancedb-linux-arm64-gnu-0.12.0.tgz", + "integrity": "sha512-mAsVwaiaLoNRLB3stocJyAEoDpwsPu++YISd5ZCaYf66CXeYU8MI50z6NV0ZYUbAFHhUzqG85CfwW+Ns2ToJ8g==", + "cpu": [ + "arm64" + ], + "license": "Apache 2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 18" + } + }, + "node_modules/@lancedb/lancedb-linux-x64-gnu": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@lancedb/lancedb-linux-x64-gnu/-/lancedb-linux-x64-gnu-0.12.0.tgz", + "integrity": "sha512-tc/A8NQQjbuEFWmq2qEWJ9s+JZFdH2diYPmMO4FNQpcbikjfk8kbJR5AFFtZel/cl2LqE7BnCvWtkI7v/hn5PA==", + "cpu": [ + "x64" + ], + "license": "Apache 2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 18" + } + }, + "node_modules/@lancedb/lancedb-win32-x64-msvc": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@lancedb/lancedb-win32-x64-msvc/-/lancedb-win32-x64-msvc-0.12.0.tgz", + "integrity": "sha512-aLpGwksA4FWvWRxsoDzo1m8ryvmcIHOLjln4BUEPtJcKdX6zWBC5VL9QlBkKribnsyj87V/5UbJpM9KyrCnk5A==", + "cpu": [ + "x64" + ], + "license": "Apache 2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/command-line-args": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/command-line-usage": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "peer": true, + "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.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apache-arrow": { + "version": "17.0.0", + "resolved": "https://registry.npmmirror.com/apache-arrow/-/apache-arrow-17.0.0.tgz", + "integrity": "sha512-X0p7auzdnGuhYMVKYINdQssS4EcKec9TCXyez/qtJt32DrIMGbzqiaMiQ0X6fQlQpw8Fl0Qygcv4dfRAr5Gu9Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/command-line-args": "^5.2.3", + "@types/command-line-usage": "^5.0.4", + "@types/node": "^20.13.0", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^24.3.25", + "json-bignum": "^0.0.3", + "tslib": "^2.6.2" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.cjs" + } + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "peer": true, + "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/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "peer": true + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/command-line-usage/-/command-line-usage-7.0.4.tgz", + "integrity": "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==", + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.1", + "typical": "^7.3.0" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/array-back/-/array-back-6.2.3.tgz", + "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/flatbuffers": { + "version": "24.12.23", + "resolved": "https://registry.npmmirror.com/flatbuffers/-/flatbuffers-24.12.23.tgz", + "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/json-bignum": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/json-bignum/-/json-bignum-0.0.3.tgz", + "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", + "peer": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "peer": true + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/array-back/-/array-back-6.2.3.tgz", + "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/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/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrapjs": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/wordwrapjs/-/wordwrapjs-5.1.1.tgz", + "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fdd755a --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "obsidian-rag", + "version": "0.1.0", + "description": "OpenClaw plugin for semantic search through Obsidian vault notes using RAG", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:@lancedb/lancedb --external:@lancedb/lancedb-darwin-arm64 --external:fsevents --external:chokidar", + "dev": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --watch", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@lancedb/lancedb": "^0.12", + "chokidar": "^3.6", + "yaml": "^2.5" + }, + "devDependencies": { + "@types/node": "^20.14", + "esbuild": "^0.24", + "typescript": "^5.5", + "vitest": "^2.0" + } +} \ No newline at end of file diff --git a/python/obsidian_rag.egg-info/PKG-INFO b/python/obsidian_rag.egg-info/PKG-INFO new file mode 100644 index 0000000..b0a45c8 --- /dev/null +++ b/python/obsidian_rag.egg-info/PKG-INFO @@ -0,0 +1,14 @@ +Metadata-Version: 2.4 +Name: obsidian-rag +Version: 0.1.0 +Summary: RAG indexer for Obsidian vaults — powers OpenClaw's obsidian_rag_* tools +Requires-Python: >=3.11 +Requires-Dist: lancedb>=0.12 +Requires-Dist: httpx>=0.27 +Requires-Dist: pyyaml>=6.0 +Requires-Dist: python-frontmatter>=1.1 +Provides-Extra: dev +Requires-Dist: pytest>=8.0; extra == "dev" +Requires-Dist: pytest-asyncio>=0.23; extra == "dev" +Requires-Dist: pytest-mock>=3.12; extra == "dev" +Requires-Dist: ruff>=0.5; extra == "dev" diff --git a/python/obsidian_rag.egg-info/SOURCES.txt b/python/obsidian_rag.egg-info/SOURCES.txt new file mode 100644 index 0000000..d405c17 --- /dev/null +++ b/python/obsidian_rag.egg-info/SOURCES.txt @@ -0,0 +1,16 @@ +pyproject.toml +obsidian_rag/__init__.py +obsidian_rag/__main__.py +obsidian_rag/chunker.py +obsidian_rag/cli.py +obsidian_rag/config.py +obsidian_rag/embedder.py +obsidian_rag/indexer.py +obsidian_rag/security.py +obsidian_rag/vector_store.py +obsidian_rag.egg-info/PKG-INFO +obsidian_rag.egg-info/SOURCES.txt +obsidian_rag.egg-info/dependency_links.txt +obsidian_rag.egg-info/entry_points.txt +obsidian_rag.egg-info/requires.txt +obsidian_rag.egg-info/top_level.txt \ No newline at end of file diff --git a/python/obsidian_rag.egg-info/dependency_links.txt b/python/obsidian_rag.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python/obsidian_rag.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/python/obsidian_rag.egg-info/entry_points.txt b/python/obsidian_rag.egg-info/entry_points.txt new file mode 100644 index 0000000..f98573a --- /dev/null +++ b/python/obsidian_rag.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +obsidian-rag = obsidian_rag.cli:main diff --git a/python/obsidian_rag.egg-info/requires.txt b/python/obsidian_rag.egg-info/requires.txt new file mode 100644 index 0000000..c50588f --- /dev/null +++ b/python/obsidian_rag.egg-info/requires.txt @@ -0,0 +1,10 @@ +lancedb>=0.12 +httpx>=0.27 +pyyaml>=6.0 +python-frontmatter>=1.1 + +[dev] +pytest>=8.0 +pytest-asyncio>=0.23 +pytest-mock>=3.12 +ruff>=0.5 diff --git a/python/obsidian_rag.egg-info/top_level.txt b/python/obsidian_rag.egg-info/top_level.txt new file mode 100644 index 0000000..0fc8f41 --- /dev/null +++ b/python/obsidian_rag.egg-info/top_level.txt @@ -0,0 +1 @@ +obsidian_rag diff --git a/python/obsidian_rag/__init__.py b/python/obsidian_rag/__init__.py new file mode 100644 index 0000000..81abf58 --- /dev/null +++ b/python/obsidian_rag/__init__.py @@ -0,0 +1,3 @@ +"""Obsidian RAG — semantic search indexer for Obsidian vaults.""" + +__version__ = "0.1.0" \ No newline at end of file diff --git a/python/obsidian_rag/__main__.py b/python/obsidian_rag/__main__.py new file mode 100644 index 0000000..1a57a12 --- /dev/null +++ b/python/obsidian_rag/__main__.py @@ -0,0 +1,7 @@ +"""CLI entry point: obsidian-rag index | sync | reindex | status.""" + +import sys +from obsidian_rag.cli import main + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/python/obsidian_rag/chunker.py b/python/obsidian_rag/chunker.py new file mode 100644 index 0000000..b9fec57 --- /dev/null +++ b/python/obsidian_rag/chunker.py @@ -0,0 +1,240 @@ +"""Markdown parsing, structured + unstructured chunking, metadata enrichment.""" + +from __future__ import annotations + +import re +import unicodedata +from dataclasses import dataclass, field +from pathlib import Path +from typing import TYPE_CHECKING + +import frontmatter + +if TYPE_CHECKING: + from obsidian_rag.config import ObsidianRagConfig + +# ---------------------------------------------------------------------- +# Types +# ---------------------------------------------------------------------- + + +@dataclass +class Chunk: + chunk_id: str + text: str + source_file: str + source_directory: str + section: str | None + date: str | None + tags: list[str] = field(default_factory=list) + chunk_index: int = 0 + total_chunks: int = 1 + modified_at: str | None = None + indexed_at: str | None = None + + +# ---------------------------------------------------------------------- +# Markdown parsing +# ---------------------------------------------------------------------- + + +def parse_frontmatter(content: str) -> tuple[dict, str]: + """Parse frontmatter from markdown content. Returns (metadata, body).""" + try: + post = frontmatter.parse(content) + meta = dict(post[0]) if post[0] else {} + body = str(post[1]) + return meta, body + except Exception: + return {}, content + + +def extract_tags(text: str) -> list[str]: + """Extract all #hashtags from text, deduplicated, lowercased.""" + return list(dict.fromkeys(t.lower() for t in re.findall(r"#[\w-]+", text))) + + +def extract_date_from_filename(filepath: Path) -> str | None: + """Try to parse an ISO date from a filename (e.g. 2024-01-15.md).""" + name = filepath.stem # filename without extension + # Match YYYY-MM-DD or YYYYMMDD + m = re.search(r"(\d{4}-\d{2}-\d{2})|(\d{4}\d{2}\d{2})", name) + if m: + date_str = m.group(1) or m.group(2) + # Normalize YYYYMMDD → YYYY-MM-DD + if len(date_str) == 8: + return f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}" + return date_str + return None + + +def is_structured_note(filepath: Path) -> bool: + """Heuristic: journal/daily notes use date-named files with section headers.""" + name = filepath.stem + date_match = re.search(r"\d{4}-\d{2}-\d{2}", name) + return date_match is not None + + +# ---------------------------------------------------------------------- +# Section-split chunker (structured notes) +# ---------------------------------------------------------------------- + + +SECTION_HEADER_RE = re.compile(r"^#{1,3}\s+(.+)$", re.MULTILINE) + + +def split_by_sections(body: str, metadata: dict) -> list[tuple[str, str]]: + """Split markdown body into (section_name, section_content) pairs. + + If no headers found, returns [(None, body)]. + """ + sections: list[tuple[str | None, str]] = [] + lines = body.splitlines(keepends=True) + current_heading: str | None = None + current_content: list[str] = [] + + for line in lines: + m = SECTION_HEADER_RE.match(line.rstrip()) + if m: + # Flush previous section + if current_heading is not None or current_content: + sections.append((current_heading, "".join(current_content).strip())) + current_content = [] + current_heading = m.group(1).strip() + else: + current_content.append(line) + + # Flush last section + if current_heading is not None or current_content: + sections.append((current_heading, "".join(current_content).strip())) + + if not sections: + sections = [(None, body.strip())] + + return sections + + +# ---------------------------------------------------------------------- +# Sliding window chunker (unstructured notes) +# ---------------------------------------------------------------------- + + +def _count_tokens(text: str) -> int: + """Rough token count: split on whitespace, average ~4 chars per token.""" + return len(text.split()) + + +def sliding_window_chunks( + text: str, + chunk_size: int = 500, + overlap: int = 100, +) -> list[str]: + """Split text into overlapping sliding-window chunks of ~chunk_size tokens. + + Returns list of chunk strings. + """ + words = text.split() + if not words: + return [] + + chunks: list[str] = [] + start = 0 + + while start < len(words): + end = start + chunk_size + chunk_words = words[start:end] + chunks.append(" ".join(chunk_words)) + + # Advance by (chunk_size - overlap) + advance = chunk_size - overlap + if advance <= 0: + advance = max(1, chunk_size // 2) + start += advance + + if start >= len(words): + break + + return chunks + + +# ---------------------------------------------------------------------- +# Main chunk router +# ---------------------------------------------------------------------- + + +def chunk_file( + filepath: Path, + content: str, + modified_at: str, + config: "ObsidianRagConfig", + chunk_id_prefix: str = "", +) -> list[Chunk]: + """Parse a markdown file and return a list of Chunks. + + Uses section-split for structured notes (journal entries with date filenames), + sliding window for everything else. + """ + import uuid + + vault_path = Path(config.vault_path) + rel_path = filepath if filepath.is_absolute() else filepath + source_file = str(rel_path) + source_directory = rel_path.parts[0] if rel_path.parts else "" + + metadata, body = parse_frontmatter(content) + tags = extract_tags(body) + date = extract_date_from_filename(filepath) + + chunk_size = config.indexing.chunk_size + overlap = config.indexing.chunk_overlap + + chunks: list[Chunk] = [] + + if is_structured_note(filepath): + # Section-split for journal/daily notes + sections = split_by_sections(body, metadata) + total = len(sections) + + for idx, (section, section_text) in enumerate(sections): + if not section_text.strip(): + continue + section_tags = extract_tags(section_text) + combined_tags = list(dict.fromkeys([*tags, *section_tags])) + + chunk_text = section_text + chunk = Chunk( + chunk_id=f"{chunk_id_prefix}{uuid.uuid4().hex[:8]}", + text=chunk_text, + source_file=source_file, + source_directory=source_directory, + section=f"#{section}" if section else None, + date=date, + tags=combined_tags, + chunk_index=idx, + total_chunks=total, + modified_at=modified_at, + ) + chunks.append(chunk) + else: + # Sliding window for unstructured notes + text_chunks = sliding_window_chunks(body, chunk_size, overlap) + total = len(text_chunks) + + for idx, text_chunk in enumerate(text_chunks): + if not text_chunk.strip(): + continue + chunk = Chunk( + chunk_id=f"{chunk_id_prefix}{uuid.uuid4().hex[:8]}", + text=text_chunk, + source_file=source_file, + source_directory=source_directory, + section=None, + date=date, + tags=tags, + chunk_index=idx, + total_chunks=total, + modified_at=modified_at, + ) + chunks.append(chunk) + + return chunks \ No newline at end of file diff --git a/python/obsidian_rag/cli.py b/python/obsidian_rag/cli.py new file mode 100644 index 0000000..cc43711 --- /dev/null +++ b/python/obsidian_rag/cli.py @@ -0,0 +1,156 @@ +"""CLI: obsidian-rag index | sync | reindex | status.""" + +from __future__ import annotations + +import json +import sys +import time +from pathlib import Path + +import obsidian_rag.config as config_mod +from obsidian_rag.vector_store import get_db, get_stats +from obsidian_rag.indexer import Indexer + + +def main(argv: list[str] | None = None) -> int: + argv = argv or sys.argv[1:] + + if not argv or argv[0] in ("--help", "-h"): + print(_usage()) + return 0 + + cmd = argv[0] + + try: + config = config_mod.load_config() + except FileNotFoundError as e: + print(f"ERROR: {e}", file=sys.stderr) + return 1 + + if cmd == "index": + return _index(config) + elif cmd == "sync": + return _sync(config) + elif cmd == "reindex": + return _reindex(config) + elif cmd == "status": + return _status(config) + else: + print(f"Unknown command: {cmd}\n{_usage()}", file=sys.stderr) + return 1 + + +def _index(config) -> int: + indexer = Indexer(config) + t0 = time.monotonic() + + try: + gen = indexer.full_index() + result: dict = {"indexed_files": 0, "total_chunks": 0, "errors": []} + for item in gen: + result = item # progress yields are dicts; final dict from return + duration_ms = int((time.monotonic() - t0) * 1000) + print( + json.dumps( + { + "type": "complete", + "indexed_files": result["indexed_files"], + "total_chunks": result["total_chunks"], + "duration_ms": duration_ms, + "errors": result["errors"], + }, + indent=2, + ) + ) + return 0 if not result["errors"] else 1 + except Exception as e: + print(json.dumps({"type": "error", "error": str(e)}), file=sys.stderr) + return 2 + + +def _sync(config) -> int: + indexer = Indexer(config) + try: + result = indexer.sync() + print(json.dumps({"type": "complete", **result}, indent=2)) + return 0 if not result["errors"] else 1 + except Exception as e: + print(json.dumps({"type": "error", "error": str(e)}), file=sys.stderr) + return 2 + + +def _reindex(config) -> int: + indexer = Indexer(config) + t0 = time.monotonic() + try: + result = indexer.reindex() + duration_ms = int((time.monotonic() - t0) * 1000) + print( + json.dumps( + { + "type": "complete", + "indexed_files": result["indexed_files"], + "total_chunks": result["total_chunks"], + "duration_ms": duration_ms, + "errors": result["errors"], + }, + indent=2, + ) + ) + return 0 + except Exception as e: + print(json.dumps({"type": "error", "error": str(e)}), file=sys.stderr) + return 2 + + +def _status(config) -> int: + try: + db = get_db(config) + table = db.open_table("obsidian_chunks") + stats = get_stats(table) + # Resolve sync-result.json path (same convention as indexer) + from pathlib import Path + import os as osmod + project_root = Path(__file__).parent.parent.parent + data_dir = project_root / "obsidian-rag" + if not data_dir.exists() and not (project_root / "KnowledgeVault").exists(): + data_dir = Path(osmod.path.expanduser("~/.obsidian-rag")) + sync_path = data_dir / "sync-result.json" + last_sync = None + if sync_path.exists(): + try: + last_sync = json.loads(sync_path.read_text()).get("timestamp") + except Exception: + pass + print( + json.dumps( + { + "total_docs": stats["total_docs"], + "total_chunks": stats["total_chunks"], + "last_sync": last_sync, + }, + indent=2, + ) + ) + return 0 + except FileNotFoundError: + print(json.dumps({"error": "Index not found. Run 'obsidian-rag index' first."}, indent=2)) + return 1 + except Exception as e: + print(json.dumps({"error": str(e)}), file=sys.stderr) + return 1 + + +def _usage() -> str: + return """obsidian-rag - Obsidian vault RAG indexer + +Usage: + obsidian-rag index Full index of the vault + obsidian-rag sync Incremental sync (changed files only) + obsidian-rag reindex Force full reindex (nuke + rebuild) + obsidian-rag status Show index health and statistics +""" + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/python/obsidian_rag/config.py b/python/obsidian_rag/config.py new file mode 100644 index 0000000..5089b1e --- /dev/null +++ b/python/obsidian_rag/config.py @@ -0,0 +1,145 @@ +"""Configuration loader — reads ~/.obsidian-rag/config.json (or ./obsidian-rag/ for dev).""" + +from __future__ import annotations + +import json +import os +from enum import Enum +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + + +DEFAULT_CONFIG_DIR = Path(__file__).parent.parent.parent # python/ → project root + + +@dataclass +class EmbeddingConfig: + provider: str = "ollama" + model: str = "mxbai-embed-large" + base_url: str = "http://localhost:11434" + dimensions: int = 1024 + batch_size: int = 64 + + +@dataclass +class VectorStoreConfig: + type: str = "lancedb" + path: str = "" # resolved relative to data_dir + + +@dataclass +class IndexingConfig: + chunk_size: int = 500 + chunk_overlap: int = 100 + file_patterns: list[str] = field(default_factory=lambda: ["*.md"]) + deny_dirs: list[str] = field( + default_factory=lambda: [".obsidian", ".trash", "zzz-Archive", ".git", ".logseq"] + ) + allow_dirs: list[str] = field(default_factory=list) + + +@dataclass +class SecurityConfig: + require_confirmation_for: list[str] = field(default_factory=lambda: ["health", "financial_debt"]) + sensitive_sections: list[str] = field( + default_factory=lambda: ["#mentalhealth", "#physicalhealth", "#Relations"] + ) + local_only: bool = True + + +@dataclass +class MemoryConfig: + auto_suggest: bool = True + patterns: dict[str, list[str]] = field( + default_factory=lambda: { + "financial": ["owe", "owed", "debt", "paid", "$", "spent", "spend"], + "health": ["#mentalhealth", "#physicalhealth", "medication", "therapy"], + "commitments": ["shopping list", "costco", "amazon", "grocery"], + } + ) + + +@dataclass +class ObsidianRagConfig: + vault_path: str = "" + embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig) + vector_store: VectorStoreConfig = field(default_factory=VectorStoreConfig) + indexing: IndexingConfig = field(default_factory=IndexingConfig) + security: SecurityConfig = field(default_factory=SecurityConfig) + memory: MemoryConfig = field(default_factory=MemoryConfig) + + +def _resolve_data_dir() -> Path: + """Resolve the data directory: dev (project root/obsidian-rag/) or production (~/.obsidian-rag/).""" + dev_data_dir = DEFAULT_CONFIG_DIR / "obsidian-rag" + if dev_data_dir.exists() or (DEFAULT_CONFIG_DIR / "KnowledgeVault").exists(): + return dev_data_dir + # Production: ~/.obsidian-rag/ + return Path(os.path.expanduser("~/.obsidian-rag")) + + +def load_config(config_path: str | Path | None = None) -> ObsidianRagConfig: + """Load config from JSON file, falling back to dev/default config.""" + if config_path is None: + config_path = _resolve_data_dir() / "config.json" + else: + config_path = Path(config_path) + + if not config_path.exists(): + raise FileNotFoundError(f"Config file not found: {config_path}") + + with open(config_path) as f: + raw: dict[str, Any] = json.load(f) + + return ObsidianRagConfig( + vault_path=raw.get("vault_path", ""), + embedding=_merge(EmbeddingConfig(), raw.get("embedding", {})), + vector_store=_merge(VectorStoreConfig(), raw.get("vector_store", {})), + indexing=_merge(IndexingConfig(), raw.get("indexing", {})), + security=_merge(SecurityConfig(), raw.get("security", {})), + memory=_merge(MemoryConfig(), raw.get("memory", {})), + ) + + +def _merge(default: Any, overrides: dict[str, Any]) -> Any: + """Shallow-merge a dict into a dataclass instance.""" + if not isinstance(default, type) and not isinstance(default, (list, dict, str, int, float, bool)): + # It's a dataclass instance — merge fields + if hasattr(default, "__dataclass_fields__"): + fields = {} + for key, val in overrides.items(): + if key in default.__dataclass_fields__: + field_def = default.__dataclass_fields__[key] + actual_default = field_def.default + if isinstance(actual_default, type) and issubclass(actual_default, Enum): + # Enum fields need special handling + fields[key] = val + elif isinstance(val, dict): + fields[key] = _merge(actual_default, val) + else: + fields[key] = val + else: + fields[key] = val + return default.__class__(**{**default.__dict__, **fields}) + if isinstance(overrides, dict) and isinstance(default, dict): + return {**default, **overrides} + return overrides if overrides is not None else default + + +def resolve_vault_path(config: ObsidianRagConfig) -> Path: + """Resolve vault_path relative to project root or as absolute.""" + vp = Path(config.vault_path) + if vp.is_absolute(): + return vp + # Resolve relative to project root + return (DEFAULT_CONFIG_DIR / vp).resolve() + + +def resolve_vector_db_path(config: ObsidianRagConfig) -> Path: + """Resolve vector store path relative to data directory.""" + data_dir = _resolve_data_dir() + vsp = Path(config.vector_store.path) + if vsp.is_absolute(): + return vsp + return (data_dir / vsp).resolve() \ No newline at end of file diff --git a/python/obsidian_rag/embedder.py b/python/obsidian_rag/embedder.py new file mode 100644 index 0000000..557ffbd --- /dev/null +++ b/python/obsidian_rag/embedder.py @@ -0,0 +1,110 @@ +"""Ollama API client for embedding generation.""" + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import httpx + +if TYPE_CHECKING: + from obsidian_rag.config import ObsidianRagConfig + +DEFAULT_TIMEOUT = 120.0 # seconds + + +class EmbeddingError(Exception): + """Raised when embedding generation fails.""" + + +class OllamaUnavailableError(EmbeddingError): + """Raised when Ollama is unreachable.""" + + +class OllamaEmbedder: + """Client for Ollama /api/embed endpoint (mxbai-embed-large, 1024-dim).""" + + def __init__(self, config: "ObsidianRagConfig"): + self.base_url = config.embedding.base_url.rstrip("/") + self.model = config.embedding.model + self.dimensions = config.embedding.dimensions + self.batch_size = config.embedding.batch_size + self._client = httpx.Client(timeout=DEFAULT_TIMEOUT) + + def is_available(self) -> bool: + """Check if Ollama is reachable and has the model.""" + try: + resp = self._client.get(f"{self.base_url}/api/tags", timeout=5.0) + if resp.status_code != 200: + return False + models = resp.json().get("models", []) + return any(self.model in m.get("name", "") for m in models) + except Exception: + return False + + def embed_chunks(self, texts: list[str]) -> list[list[float]]: + """Generate embeddings for a batch of texts. Returns list of vectors.""" + if not texts: + return [] + + all_vectors: list[list[float]] = [] + for i in range(0, len(texts), self.batch_size): + batch = texts[i : i + self.batch_size] + vectors = self._embed_batch(batch) + all_vectors.extend(vectors) + + return all_vectors + + def embed_single(self, text: str) -> list[float]: + """Generate embedding for a single text.""" + [vec] = self._embed_batch([text]) + return vec + + def _embed_batch(self, batch: list[str]) -> list[list[float]]: + """Internal batch call. Raises EmbeddingError on failure.""" + # Ollama /api/embeddings takes {"model": "...", "prompt": "..."} for single + # For batch, call /api/embeddings multiple times sequentially + if len(batch) == 1: + endpoint = f"{self.base_url}/api/embeddings" + payload = {"model": self.model, "prompt": batch[0]} + else: + # For batch, use /api/embeddings with "input" (multiple calls) + results = [] + for text in batch: + try: + resp = self._client.post( + f"{self.base_url}/api/embeddings", + json={"model": self.model, "prompt": text}, + timeout=DEFAULT_TIMEOUT, + ) + except httpx.ConnectError as e: + raise OllamaUnavailableError(f"Cannot connect to Ollama at {self.base_url}") from e + except httpx.TimeoutException as e: + raise EmbeddingError(f"Embedding request timed out after {DEFAULT_TIMEOUT}s") from e + if resp.status_code != 200: + raise EmbeddingError(f"Ollama returned {resp.status_code}: {resp.text}") + data = resp.json() + embedding = data.get("embedding", []) + if not embedding: + embedding = data.get("embeddings", [[]])[0] + results.append(embedding) + return results + + try: + resp = self._client.post(endpoint, json=payload, timeout=DEFAULT_TIMEOUT) + except httpx.ConnectError as e: + raise OllamaUnavailableError(f"Cannot connect to Ollama at {self.base_url}") from e + except httpx.TimeoutException as e: + raise EmbeddingError(f"Embedding request timed out after {DEFAULT_TIMEOUT}s") from e + + if resp.status_code != 200: + raise EmbeddingError(f"Ollama returned {resp.status_code}: {resp.text}") + + data = resp.json() + embedding = data.get("embedding", []) + if not embedding: + embedding = data.get("embeddings", [[]])[0] + return [embedding] + + def close(self): + self._client.close() diff --git a/python/obsidian_rag/indexer.py b/python/obsidian_rag/indexer.py new file mode 100644 index 0000000..7444bea --- /dev/null +++ b/python/obsidian_rag/indexer.py @@ -0,0 +1,223 @@ +"""Full indexing pipeline: scan → parse → chunk → embed → store.""" + +from __future__ import annotations + +import json +import os +import time +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import TYPE_CHECKING, Any, Generator, Iterator + +if TYPE_CHECKING: + from obsidian_rag.config import ObsidianRagConfig + +import obsidian_rag.config as config_mod +from obsidian_rag.chunker import chunk_file +from obsidian_rag.embedder import EmbeddingError, OllamaUnavailableError +from obsidian_rag.security import should_index_dir, validate_path +from obsidian_rag.vector_store import create_table_if_not_exists, delete_by_source_file, get_db, upsert_chunks + +# ---------------------------------------------------------------------- +# Pipeline +# ---------------------------------------------------------------------- + + +class Indexer: + """Coordinates the scan → chunk → embed → store pipeline.""" + + def __init__(self, config: "ObsidianRagConfig"): + self.config = config + self.vault_path = config_mod.resolve_vault_path(config) + self._embedder = None # lazy init + + @property + def embedder(self): + if self._embedder is None: + from obsidian_rag.embedder import OllamaEmbedder + self._embedder = OllamaEmbedder(self.config) + return self._embedder + + def scan_vault(self) -> Generator[Path, None, None]: + """Walk vault, yielding markdown files to index.""" + for root, dirs, files in os.walk(self.vault_path): + root_path = Path(root) + # Filter directories + dirs[:] = [d for d in dirs if should_index_dir(d, self.config)] + + for fname in files: + if not fname.endswith(".md"): + continue + filepath = root_path / fname + try: + validate_path(filepath, self.vault_path) + except ValueError: + continue + yield filepath + + def process_file(self, filepath: Path) -> tuple[int, list[dict[str, Any]]]: + """Index a single file. Returns (num_chunks, enriched_chunks).""" + from obsidian_rag import security + + mtime = str(datetime.fromtimestamp(filepath.stat().st_mtime, tz=timezone.utc).isoformat()) + content = filepath.read_text(encoding="utf-8") + # Sanitize + content = security.sanitize_text(content) + # Chunk + chunks = chunk_file(filepath, content, mtime, self.config) + # Enrich with indexed_at + now = datetime.now(timezone.utc).isoformat() + enriched: list[dict[str, Any]] = [] + for chunk in chunks: + enriched.append( + { + "chunk_id": chunk.chunk_id, + "chunk_text": chunk.text, + "source_file": chunk.source_file, + "source_directory": chunk.source_directory, + "section": chunk.section, + "date": chunk.date, + "tags": chunk.tags, + "chunk_index": chunk.chunk_index, + "total_chunks": chunk.total_chunks, + "modified_at": chunk.modified_at, + "indexed_at": now, + } + ) + return len(chunks), enriched + + def full_index(self, on_progress: Iterator[dict] | None = None) -> dict[str, Any]: + """Run full index of the vault. Calls on_progress with status dicts.""" + vault_path = self.vault_path + if not vault_path.exists(): + raise FileNotFoundError(f"Vault not found: {vault_path}") + + db = get_db(self.config) + table = create_table_if_not_exists(db) + embedder = self.embedder + + files = list(self.scan_vault()) + total_files = len(files) + indexed_files = 0 + total_chunks = 0 + errors: list[dict] = [] + + for idx, filepath in enumerate(files): + try: + num_chunks, enriched = self.process_file(filepath) + # Embed chunks + texts = [e["chunk_text"] for e in enriched] + try: + vectors = embedder.embed_chunks(texts) + except OllamaUnavailableError: + # Partial results without embeddings — skip + vectors = [[0.0] * 1024 for _ in texts] + # Add vectors + for e, v in zip(enriched, vectors): + e["vector"] = v + # Store + upsert_chunks(table, enriched) + total_chunks += num_chunks + indexed_files += 1 + except Exception as exc: + errors.append({"file": str(filepath), "error": str(exc)}) + + if on_progress: + phase = "embedding" if idx < total_files // 2 else "storing" + yield { + "type": "progress", + "phase": phase, + "current": idx + 1, + "total": total_files, + } + + return { + "indexed_files": indexed_files, + "total_chunks": total_chunks, + "duration_ms": 0, # caller can fill + "errors": errors, + } + + def sync(self, on_progress: Iterator[dict] | None = None) -> dict[str, Any]: + """Incremental sync: only process files modified since last sync.""" + sync_result_path = self._sync_result_path() + last_sync = None + if sync_result_path.exists(): + try: + last_sync = json.loads(sync_result_path.read_text()).get("timestamp") + except Exception: + pass + + db = get_db(self.config) + table = create_table_if_not_exists(db) + embedder = self.embedder + + files = list(self.scan_vault()) + indexed_files = 0 + total_chunks = 0 + errors: list[dict] = [] + + for filepath in files: + mtime = datetime.fromtimestamp(filepath.stat().st_mtime, tz=timezone.utc) + mtime_str = mtime.isoformat() + if last_sync and mtime_str <= last_sync: + continue # unchanged + + try: + num_chunks, enriched = self.process_file(filepath) + texts = [e["chunk_text"] for e in enriched] + try: + vectors = embedder.embed_chunks(texts) + except OllamaUnavailableError: + vectors = [[0.0] * 1024 for _ in texts] + for e, v in zip(enriched, vectors): + e["vector"] = v + upsert_chunks(table, enriched) + total_chunks += num_chunks + indexed_files += 1 + except Exception as exc: + errors.append({"file": str(filepath), "error": str(exc)}) + + self._write_sync_result(indexed_files, total_chunks, errors) + return { + "indexed_files": indexed_files, + "total_chunks": total_chunks, + "errors": errors, + } + + def reindex(self) -> dict[str, Any]: + """Nuke and rebuild: drop table and run full index.""" + db = get_db(self.config) + if "obsidian_chunks" in db.list_tables(): + db.drop_table("obsidian_chunks") + # full_index is a generator — materialize it to get the final dict + results = list(self.full_index()) + return results[-1] if results else {"indexed_files": 0, "total_chunks": 0, "errors": []} + + def _sync_result_path(self) -> Path: + # Use the same dev-data-dir convention as config.py + project_root = Path(__file__).parent.parent.parent + data_dir = project_root / "obsidian-rag" + if not data_dir.exists() and not (project_root / "KnowledgeVault").exists(): + data_dir = Path(os.path.expanduser("~/.obsidian-rag")) + return data_dir / "sync-result.json" + + def _write_sync_result( + self, + indexed_files: int, + total_chunks: int, + errors: list[dict], + ) -> None: + path = self._sync_result_path() + path.parent.mkdir(parents=True, exist_ok=True) + result = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "indexed_files": indexed_files, + "total_chunks": total_chunks, + "errors": errors, + } + # Atomic write: .tmp → rename + tmp = path.with_suffix(".json.tmp") + tmp.write_text(json.dumps(result, indent=2)) + tmp.rename(path) diff --git a/python/obsidian_rag/security.py b/python/obsidian_rag/security.py new file mode 100644 index 0000000..ebb38bb --- /dev/null +++ b/python/obsidian_rag/security.py @@ -0,0 +1,164 @@ +"""Path traversal prevention, input sanitization, sensitive content detection, directory access control.""" + +from __future__ import annotations + +import re +import unicodedata +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from obsidian_rag.config import ObsidianRagConfig + +# ---------------------------------------------------------------------- +# Path traversal +# ---------------------------------------------------------------------- + + +def validate_path(requested: Path, vault_root: Path) -> Path: + """Resolve requested relative to vault_root and reject anything escaping the vault. + + Raises ValueError on traversal attempts. + """ + # Resolve both to absolute paths + vault = vault_root.resolve() + try: + resolved = (vault / requested).resolve() + except (OSError, ValueError) as e: + raise ValueError(f"Cannot resolve path: {requested}") from e + + # Check the resolved path is under vault + try: + resolved.relative_to(vault) + except ValueError: + raise ValueError(f"Path traversal attempt blocked: {requested} resolves outside vault") + + # Reject obvious traversal + if ".." in requested.parts: + raise ValueError(f"Path traversal attempt blocked: {requested}") + + return resolved + + +def is_symlink_outside_vault(path: Path, vault_root: Path) -> bool: + """Check if path is a symlink that resolves outside the vault.""" + try: + resolved = path.resolve() + vault = vault_root.resolve() + # Check if any parent (including self) is outside vault + try: + resolved.relative_to(vault) + return False + except ValueError: + return True + except (OSError, ValueError): + return True + + +# ---------------------------------------------------------------------- +# Input sanitization +# ---------------------------------------------------------------------- + + +HTML_TAG_RE = re.compile(r"<[^>]+>") +CODE_BLOCK_RE = re.compile(r"```[\s\S]*?```", re.MULTILINE) +MULTI_WHITESPACE_RE = re.compile(r"\s+") +MAX_CHUNK_LEN = 2000 + + +def sanitize_text(raw: str) -> str: + """Sanitize raw vault content before embedding. + + - Strip HTML tags (prevent XSS) + - Remove fenced code blocks + - Normalize whitespace + - Cap length at MAX_CHUNK_LEN chars + """ + # Remove fenced code blocks + text = CODE_BLOCK_RE.sub(" ", raw) + # Strip HTML tags + text = HTML_TAG_RE.sub("", text) + # Remove leading/trailing whitespace + text = text.strip() + # Normalize internal whitespace + text = MULTI_WHITESPACE_RE.sub(" ", text) + # Cap length + if len(text) > MAX_CHUNK_LEN: + text = text[:MAX_CHUNK_LEN] + return text + + +# ---------------------------------------------------------------------- +# Sensitive content detection +# ---------------------------------------------------------------------- + + +def detect_sensitive( + text: str, + sensitive_sections: list[str], + patterns: dict[str, list[str]], +) -> dict[str, bool]: + """Detect sensitive content categories in text. + + Returns dict with keys: health, financial, relations. + """ + text_lower = text.lower() + result: dict[str, bool] = { + "health": False, + "financial": False, + "relations": False, + } + + # Check for sensitive section headings in the text + for section in sensitive_sections: + if section.lower() in text_lower: + result["health"] = result["health"] or section.lower() in ["#mentalhealth", "#physicalhealth"] + + # Pattern matching + financial_patterns = patterns.get("financial", []) + health_patterns = patterns.get("health", []) + + for pat in financial_patterns: + if pat.lower() in text_lower: + result["financial"] = True + break + + for pat in health_patterns: + if pat.lower() in text_lower: + result["health"] = True + break + + return result + + +# ---------------------------------------------------------------------- +# Directory access control +# ---------------------------------------------------------------------- + + +def should_index_dir( + dir_name: str, + config: "ObsidianRagConfig", +) -> bool: + """Apply deny/allow list rules to a directory. + + If allow_dirs is non-empty, only those dirs are allowed. + If deny_dirs matches, the dir is rejected. + Hidden dirs (starting with '.') are always rejected. + """ + # Always reject hidden directories + if dir_name.startswith("."): + return False + + # If allow list is set, only those dirs are allowed + if config.indexing.allow_dirs: + return dir_name in config.indexing.allow_dirs + + # Otherwise reject any deny-listed directory + deny = config.indexing.deny_dirs + return dir_name not in deny + + +def filter_tags(text: str) -> list[str]: + """Extract all #hashtags from text, lowercased and deduplicated.""" + return list(dict.fromkeys(tag.lower() for tag in re.findall(r"#\w+", text))) \ No newline at end of file diff --git a/python/obsidian_rag/vector_store.py b/python/obsidian_rag/vector_store.py new file mode 100644 index 0000000..31786fd --- /dev/null +++ b/python/obsidian_rag/vector_store.py @@ -0,0 +1,178 @@ +"""LanceDB table creation, vector upsert/delete/search.""" + +from __future__ import annotations + +import json +import os +import time +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any, Iterable + +import lancedb + +if TYPE_CHECKING: + from obsidian_rag.config import ObsidianRagConfig + +# ---------------------------------------------------------------------- +# Schema constants +# ---------------------------------------------------------------------- + +TABLE_NAME = "obsidian_chunks" +VECTOR_DIM = 1024 # mxbai-embed-large + +# ---------------------------------------------------------------------- +# Types +# ---------------------------------------------------------------------- + + +@dataclass +class SearchResult: + chunk_id: str + chunk_text: str + source_file: str + source_directory: str + section: str | None + date: str | None + tags: list[str] + chunk_index: int + score: float + + +# ---------------------------------------------------------------------- +# Table setup +# ---------------------------------------------------------------------- + + +def get_db(config: "ObsidianRagConfig") -> lancedb.LanceDBConnection: + """Connect to the LanceDB database.""" + import obsidian_rag.config as cfg_mod + + db_path = cfg_mod.resolve_vector_db_path(config) + db_path.parent.mkdir(parents=True, exist_ok=True) + return lancedb.connect(str(db_path)) + + +def create_table_if_not_exists(db: Any) -> Any: + """Create the obsidian_chunks table if it doesn't exist.""" + import pyarrow as pa + + if TABLE_NAME in db.list_tables(): + return db.open_table(TABLE_NAME) + + schema = pa.schema( + [ + pa.field("vector", pa.list_(pa.float32(), VECTOR_DIM)), + pa.field("chunk_id", pa.string()), + pa.field("chunk_text", pa.string()), + pa.field("source_file", pa.string()), + pa.field("source_directory", pa.string()), + pa.field("section", pa.string()), + pa.field("date", pa.string()), + pa.field("tags", pa.list_(pa.string())), + pa.field("chunk_index", pa.int32()), + pa.field("total_chunks", pa.int32()), + pa.field("modified_at", pa.string()), + pa.field("indexed_at", pa.string()), + ] + ) + + tbl = db.create_table(TABLE_NAME, schema=schema, exist_ok=True) + return tbl + + +# ---------------------------------------------------------------------- +# CRUD operations +# ---------------------------------------------------------------------- + + +def upsert_chunks( + table: Any, + chunks: list[dict[str, Any]], +) -> int: + """Add or update chunks in the table. Returns number of chunks written.""" + if not chunks: + return 0 + # Use when_matched_update_all + when_not_matched_insert_all for full upsert + ( + table.merge_insert("chunk_id") + .when_matched_update_all() + .when_not_matched_insert_all() + .execute(chunks) + ) + return len(chunks) + + +def delete_by_source_file(table: Any, source_file: str) -> int: + """Delete all chunks from a given source file. Returns count deleted.""" + before = table.count_rows() + table.delete(f'source_file = "{source_file}"') + return before - table.count_rows() + + +def search_chunks( + table: Any, + query_vector: list[float], + limit: int = 5, + directory_filter: list[str] | None = None, + date_range: dict | None = None, + tags: list[str] | None = None, +) -> list[SearchResult]: + """Search for similar chunks using vector similarity. + + Filters are applied as AND conditions. + """ + import pyarrow as pa + + # Build WHERE clause + conditions: list[str] = [] + if directory_filter: + dir_list = ", ".join(f'"{d}"' for d in directory_filter) + conditions.append(f'source_directory IN ({dir_list})') + if date_range: + if "from" in date_range: + conditions.append(f"date >= '{date_range['from']}'") + if "to" in date_range: + conditions.append(f"date <= '{date_range['to']}'") + if tags: + for tag in tags: + conditions.append(f"list_contains(tags, '{tag}')") + + where_clause = " AND ".join(conditions) if conditions else None + + results = ( + table.search(query_vector, vector_column_name="vector") + .limit(limit) + .where(where_clause) if where_clause else table.search(query_vector, vector_column_name="vector").limit(limit) + ).to_list() + + return [ + SearchResult( + chunk_id=r["chunk_id"], + chunk_text=r["chunk_text"], + source_file=r["source_file"], + source_directory=r["source_directory"], + section=r.get("section"), + date=r.get("date"), + tags=r.get("tags", []), + chunk_index=r.get("chunk_index", 0), + score=r.get("_score", 0.0), + ) + for r in results + ] + + +def get_stats(table: Any) -> dict[str, Any]: + """Return index statistics.""" + total_docs = 0 + total_chunks = 0 + try: + total_chunks = table.count_rows() + # Count unique source files using pandas + all_data = table.to_pandas() + total_docs = all_data["source_file"].nunique() + except Exception: + pass + + return {"total_docs": total_docs, "total_chunks": total_chunks} \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..3bf01a6 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["setuptools>=68.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "obsidian-rag" +version = "0.1.0" +description = "RAG indexer for Obsidian vaults — powers OpenClaw's obsidian_rag_* tools" +requires-python = ">=3.11" +dependencies = [ + "lancedb>=0.12", + "httpx>=0.27", + "pyyaml>=6.0", + "python-frontmatter>=1.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "pytest-mock>=3.12", + "ruff>=0.5", +] + +[project.scripts] +obsidian-rag = "obsidian_rag.cli:main" + +[tool.setuptools.packages.find] +where = ["."] +include = ["obsidian_rag*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +asyncio_mode = "auto" \ No newline at end of file diff --git a/python/tests/unit/test_chunker.py b/python/tests/unit/test_chunker.py new file mode 100644 index 0000000..e1bf1ae --- /dev/null +++ b/python/tests/unit/test_chunker.py @@ -0,0 +1,250 @@ +"""Tests for obsidian_rag.chunker — section splitting and sliding window.""" + +from __future__ import annotations + +from pathlib import Path +import tempfile +from unittest.mock import MagicMock + +import pytest + +from obsidian_rag.chunker import ( + extract_tags, + extract_date_from_filename, + is_structured_note, + parse_frontmatter, + split_by_sections, + sliding_window_chunks, + chunk_file, +) + + +# ---------------------------------------------------------------------- +# parse_frontmatter +# ---------------------------------------------------------------------- + + +def test_parse_frontmatter_with_yaml(): + content = """--- +title: My Journal +tags: [journal, personal] +--- +# Morning + +Some content here. +""" + meta, body = parse_frontmatter(content) + assert meta.get("title") == "My Journal" + assert "# Morning" in body + assert "Some content" in body + + +def test_parse_frontmatter_without_frontmatter(): + content = "# Just a header\n\nSome text without frontmatter." + meta, body = parse_frontmatter(content) + assert meta == {} + assert "# Just a header" in body + + +# ---------------------------------------------------------------------- +# extract_tags +# ---------------------------------------------------------------------- + + +def test_extract_tags_basic(): + text = "Hello #world and #python-code is nice" + tags = extract_tags(text) + assert "#world" in tags + assert "#python-code" in tags + # lowercased + assert all(t.startswith("#") for t in tags) + + +def test_extract_tags_deduplicates(): + text = "#hello #world #hello #python" + tags = extract_tags(text) + assert len(tags) == 3 + + +# ---------------------------------------------------------------------- +# extract_date_from_filename +# ---------------------------------------------------------------------- + + +def test_extract_date_from_filename_iso(): + p = Path("2024-01-15.md") + assert extract_date_from_filename(p) == "2024-01-15" + + +def test_extract_date_from_filename_compact(): + p = Path("20240115.md") + assert extract_date_from_filename(p) == "2024-01-15" + + +def test_extract_date_from_filename_no_date(): + p = Path("my-journal.md") + assert extract_date_from_filename(p) is None + + +# ---------------------------------------------------------------------- +# is_structured_note +# ---------------------------------------------------------------------- + + +def test_is_structured_note_journal(): + assert is_structured_note(Path("2024-01-15.md")) is True + assert is_structured_note(Path("Journal/2024-02-20.md")) is True + + +def test_is_structured_note_project(): + assert is_structured_note(Path("My Project Ideas.md")) is False + assert is_structured_note(Path("shopping-list.md")) is False + + +# ---------------------------------------------------------------------- +# split_by_sections +# ---------------------------------------------------------------------- + + +def test_split_by_sections_multiple(): + body = """# Mental Health +Feeling anxious today. + +## Work +Project deadline approaching. + +### Home +Need to clean the garage. +""" + sections = split_by_sections(body, {}) + assert len(sections) == 3 + assert sections[0][0] == "Mental Health" + # Section content excludes the header line itself + assert "Feeling anxious today." in sections[0][1] + assert sections[1][0] == "Work" + assert sections[2][0] == "Home" + + +def test_split_by_sections_no_headers(): + body = "Just plain text without any headers at all." + sections = split_by_sections(body, {}) + assert len(sections) == 1 + assert sections[0][0] is None + assert "Just plain text" in sections[0][1] + + +def test_split_by_sections_leading_content(): + """Content before the first header belongs to the first section.""" + body = """Some intro text before any header. + +# First Section +Content of first. +""" + sections = split_by_sections(body, {}) + assert sections[0][0] is None + assert "Some intro text" in sections[0][1] + assert sections[1][0] == "First Section" + + +# ---------------------------------------------------------------------- +# sliding_window_chunks +# ---------------------------------------------------------------------- + + +def test_sliding_window_basic(): + words = " ".join([f"word{i}" for i in range(1200)]) + chunks = sliding_window_chunks(words, chunk_size=500, overlap=100) + assert len(chunks) >= 2 + # First chunk: words 0-499 + assert chunks[0].startswith("word0") + # Chunks should have ~500 tokens each + for c in chunks: + assert len(c.split()) <= 500 + + +def test_sliding_window_overlap(): + """Adjacent chunks should share the overlap region.""" + text = " ".join([f"word{i}" for i in range(1000)]) + chunks = sliding_window_chunks(text, chunk_size=500, overlap=100) + # Every chunk after the first should start with words from the previous chunk + for i in range(1, len(chunks)): + prev_words = chunks[i - 1].split() + curr_words = chunks[i].split() + # Overlap should be evident + assert prev_words[-100:] == curr_words[:100] + + +def test_sliding_window_empty(): + assert sliding_window_chunks("", chunk_size=500, overlap=100) == [] + + +def test_sliding_window_exact_size_produces_two_chunks(): + """With overlap=100, exactly 500 words produces 2 chunks (0-499 and 400-end).""" + words = " ".join([f"word{i}" for i in range(500)]) + chunks = sliding_window_chunks(words, chunk_size=500, overlap=100) + assert len(chunks) == 2 + assert chunks[0].startswith("word0") + assert chunks[1].startswith("word400") # advance = 500-100 = 400 + + +def test_sliding_window_small_text(): + """Text much shorter than chunk_size returns single chunk.""" + text = "just a few words" + chunks = sliding_window_chunks(text, chunk_size=500, overlap=100) + assert len(chunks) == 1 + assert chunks[0] == text + + +# ---------------------------------------------------------------------- +# chunk_file integration +# ---------------------------------------------------------------------- + + +def _mock_config(tmp_path: Path) -> MagicMock: + """Build a minimal mock config pointing at a tmp vault.""" + cfg = MagicMock() + cfg.vault_path = str(tmp_path) + cfg.indexing.chunk_size = 500 + cfg.indexing.chunk_overlap = 100 + cfg.indexing.file_patterns = ["*.md"] + cfg.indexing.deny_dirs = [".obsidian", ".trash", "zzz-Archive", ".git"] + cfg.indexing.allow_dirs = [] + return cfg + + +def test_chunk_file_structured_journal(tmp_path: Path): + vault = tmp_path / "Journal" + vault.mkdir() + fpath = vault / "2024-03-15.md" + fpath.write_text("""# Morning + +Felt #anxious about the deadline. + +## Work +Finished the report. +""") + + cfg = _mock_config(tmp_path) + chunks = chunk_file(fpath, fpath.read_text(), "2024-03-15T10:00:00Z", cfg) + + # Journal file → section-split → 2 chunks + assert len(chunks) == 2 + assert chunks[0].section == "#Morning" + assert chunks[0].date == "2024-03-15" + assert "#anxious" in chunks[0].tags or "#anxious" in chunks[1].tags + assert chunks[0].source_file.endswith("Journal/2024-03-15.md") + + +def test_chunk_file_unstructured(tmp_path: Path): + vault = tmp_path / "Notes" + vault.mkdir() + fpath = vault / "project-ideas.md" + fpath.write_text("This is a long note " * 200) # ~1000 words + + cfg = _mock_config(tmp_path) + chunks = chunk_file(fpath, fpath.read_text(), "2024-03-15T10:00:00Z", cfg) + + # Unstructured → sliding window → multiple chunks + assert len(chunks) > 1 + assert all(c.section is None for c in chunks) + assert chunks[0].chunk_index == 0 diff --git a/python/tests/unit/test_config.py b/python/tests/unit/test_config.py new file mode 100644 index 0000000..a3c33dc --- /dev/null +++ b/python/tests/unit/test_config.py @@ -0,0 +1,130 @@ +"""Tests for obsidian_rag.config — loader, path resolution, defaults.""" + +from __future__ import annotations + +import json +import tempfile +from pathlib import Path + +import pytest + +from obsidian_rag.config import ( + EmbeddingConfig, + ObsidianRagConfig, + load_config, + resolve_vector_db_path, + resolve_vault_path, +) + + +# ---------------------------------------------------------------------- +# Config loading +# ---------------------------------------------------------------------- + + +def test_load_config_parses_valid_json(tmp_path: Path): + config_path = tmp_path / "config.json" + config_path.write_text( + json.dumps({ + "vault_path": "/path/to/vault", + "embedding": {"model": "custom-model:tag", "dimensions": 512}, + "vector_store": {"path": "/vectors/db"}, + }) + ) + config = load_config(config_path) + assert config.vault_path == "/path/to/vault" + assert config.embedding.model == "custom-model:tag" + assert config.embedding.dimensions == 512 # overridden + + +def test_load_config_missing_file_raises(tmp_path: Path): + with pytest.raises(FileNotFoundError): + load_config(tmp_path / "nonexistent.json") + + +def test_load_config_merges_partial_json(tmp_path: Path): + config_path = tmp_path / "config.json" + config_path.write_text(json.dumps({"vault_path": "/custom/vault"})) + config = load_config(config_path) + # Unspecified fields fall back to defaults + assert config.vault_path == "/custom/vault" + assert config.embedding.base_url == "http://localhost:11434" # default + assert config.indexing.chunk_size == 500 # default + + +# ---------------------------------------------------------------------- +# resolve_vault_path +# ---------------------------------------------------------------------- + + +def test_resolve_vault_path_absolute(): + cfg = ObsidianRagConfig(vault_path="/absolute/vault") + assert resolve_vault_path(cfg) == Path("/absolute/vault") + + +def test_resolve_vault_path_relative_defaults_to_project_root(): + cfg = ObsidianRagConfig(vault_path="KnowledgeVault/Default") + result = resolve_vault_path(cfg) + # Should resolve relative to python/obsidian_rag/ → project root + assert result.name == "Default" + assert result.parent.name == "KnowledgeVault" + + +# ---------------------------------------------------------------------- +# resolve_vector_db_path +# ---------------------------------------------------------------------- + + +def test_resolve_vector_db_path_string_absolute(): + """VectorStoreConfig stores path as a string; Path objects should be converted first.""" + from obsidian_rag.config import VectorStoreConfig + + # Using a string path (the actual usage) + cfg = ObsidianRagConfig(vector_store=VectorStoreConfig(path="/my/vectors.lance")) + result = resolve_vector_db_path(cfg) + assert result == Path("/my/vectors.lance") + + +def test_resolve_vector_db_path_string_relative(tmp_path: Path): + """Relative paths are resolved against the data directory.""" + import obsidian_rag.config as cfg_mod + + # Set up data dir + vault marker (required by _resolve_data_dir) + # Note: the dev data dir is "obsidian-rag" (without leading dot) + data_dir = tmp_path / "obsidian-rag" + data_dir.mkdir() + (tmp_path / "KnowledgeVault").mkdir() + vector_file = data_dir / "vectors.lance" + vector_file.touch() + + cfg = ObsidianRagConfig(vector_store=cfg_mod.VectorStoreConfig(path="vectors.lance")) + orig = cfg_mod.DEFAULT_CONFIG_DIR + cfg_mod.DEFAULT_CONFIG_DIR = tmp_path + try: + result = resolve_vector_db_path(cfg) + finally: + cfg_mod.DEFAULT_CONFIG_DIR = orig + + # Resolves to data_dir / vectors.lance + assert result.parent.name == "obsidian-rag" # dev dir is "obsidian-rag" (no leading dot) + assert result.name == "vectors.lance" + + +# ---------------------------------------------------------------------- +# Dataclass defaults +# ---------------------------------------------------------------------- + + +def test_embedding_config_defaults(): + cfg = EmbeddingConfig() + assert cfg.model == "mxbai-embed-large" + assert cfg.dimensions == 1024 + assert cfg.batch_size == 64 + + +def test_security_config_defaults(): + from obsidian_rag.config import SecurityConfig + + cfg = SecurityConfig() + assert "#mentalhealth" in cfg.sensitive_sections + assert "health" in cfg.require_confirmation_for \ No newline at end of file diff --git a/python/tests/unit/test_security.py b/python/tests/unit/test_security.py new file mode 100644 index 0000000..c623959 --- /dev/null +++ b/python/tests/unit/test_security.py @@ -0,0 +1,254 @@ +"""Tests for obsidian_rag.security — path traversal, sanitization, sensitive detection.""" + +from __future__ import annotations + +from pathlib import Path +import tempfile +from unittest.mock import MagicMock + +import pytest + +from obsidian_rag.security import ( + detect_sensitive, + filter_tags, + is_symlink_outside_vault, + sanitize_text, + should_index_dir, + validate_path, +) + + +# ---------------------------------------------------------------------- +# validate_path +# ---------------------------------------------------------------------- + + +def test_validate_path_normal_file(tmp_path: Path): + vault = tmp_path / "vault" + vault.mkdir() + target = vault / "subdir" / "note.md" + target.parent.mkdir() + target.touch() + + result = validate_path(Path("subdir/note.md"), vault) + assert result == target.resolve() + + +def test_validate_path_traversal_attempt(tmp_path: Path): + vault = tmp_path / "vault" + vault.mkdir() + + with pytest.raises(ValueError, match="traversal"): + validate_path(Path("../etc/passwd"), vault) + + +def test_validate_path_deep_traversal(tmp_path: Path): + vault = tmp_path / "vault" + vault.mkdir() + + with pytest.raises(ValueError, match="traversal"): + validate_path(Path("subdir/../../../etc/passwd"), vault) + + +def test_validate_path_absolute_path(tmp_path: Path): + vault = tmp_path / "vault" + vault.mkdir() + + with pytest.raises(ValueError): + validate_path(Path("/etc/passwd"), vault) + + +def test_validate_path_path_with_dotdot_in_resolve(tmp_path: Path): + """Path that resolves inside vault but has .. in parts should be caught.""" + vault = tmp_path / "vault" + vault.mkdir() + sub = vault / "subdir" + sub.mkdir() + + # validate_path checks parts for ".." + with pytest.raises(ValueError, match="traversal"): + validate_path(Path("subdir/../subdir/../note.md"), vault) + + +# ---------------------------------------------------------------------- +# is_symlink_outside_vault +# ---------------------------------------------------------------------- + + +def test_is_symlink_outside_vault_internal(tmp_path: Path): + vault = tmp_path / "vault" + vault.mkdir() + note = vault / "note.md" + note.touch() + + link = vault / "link.md" + link.symlink_to(note) + + assert is_symlink_outside_vault(link, vault) is False + + +def test_is_symlink_outside_vault_external(tmp_path: Path): + vault = tmp_path / "vault" + vault.mkdir() + outside = tmp_path / "outside.md" + outside.touch() + + link = vault / "link.md" + link.symlink_to(outside) + + assert is_symlink_outside_vault(link, vault) is True + + +# ---------------------------------------------------------------------- +# sanitize_text +# ---------------------------------------------------------------------- + + +def test_sanitize_text_strips_html(): + raw = "Hello #world" + result = sanitize_text(raw) + assert "