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 "