diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e993fd..b931bea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: os: windows-latest archive: zip - target: x86_64-apple-darwin - os: macos-latest + os: macos-13 archive: tar.gz - target: aarch64-apple-darwin os: macos-latest diff --git a/Cargo.lock b/Cargo.lock index 08f4de0..3d6af25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" dependencies = [ "clap_builder", "clap_derive", @@ -352,9 +352,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" dependencies = [ "anstream", "anstyle", @@ -364,9 +364,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -1279,9 +1279,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1430,7 +1430,7 @@ dependencies = [ "rgb", "tiff", "zune-core 0.5.1", - "zune-jpeg 0.5.11", + "zune-jpeg 0.5.12", ] [[package]] @@ -1929,9 +1929,12 @@ dependencies = [ [[package]] name = "notify-types" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.10.0", +] [[package]] name = "nu-ansi-term" @@ -2075,9 +2078,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.4+3.5.4" +version = "300.5.5+3.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" dependencies = [ "cc", ] @@ -3436,9 +3439,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -3477,7 +3480,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vestige-core" -version = "1.1.0" +version = "1.1.2" dependencies = [ "chrono", "directories", @@ -3511,7 +3514,7 @@ dependencies = [ [[package]] name = "vestige-mcp" -version = "1.1.1" +version = "1.1.2" dependencies = [ "anyhow", "chrono", @@ -3998,18 +4001,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" dependencies = [ "proc-macro2", "quote", @@ -4078,9 +4081,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" [[package]] name = "zune-core" @@ -4114,9 +4117,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2959ca473aae96a14ecedf501d20b3608d2825ba280d5adb57d651721885b0c2" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ "zune-core 0.5.1", ] diff --git a/Cargo.toml b/Cargo.toml index 4ce76ef..0d7044e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "1.0.0" +version = "1.1.2" edition = "2021" license = "AGPL-3.0-only" repository = "https://github.com/samvallad33/vestige" diff --git a/crates/vestige-mcp/src/tools/ingest.rs b/crates/vestige-mcp/src/tools/ingest.rs index 586e2f1..26c4175 100644 --- a/crates/vestige-mcp/src/tools/ingest.rs +++ b/crates/vestige-mcp/src/tools/ingest.rs @@ -76,14 +76,50 @@ pub async fn execute( }; let mut storage = storage.lock().await; - let node = storage.ingest(input).map_err(|e| e.to_string())?; - Ok(serde_json::json!({ - "success": true, - "nodeId": node.id, - "message": format!("Knowledge ingested successfully. Node ID: {}", node.id), - "hasEmbedding": node.has_embedding.unwrap_or(false), - })) + // Route through smart_ingest when embeddings are available to prevent duplicates. + // Falls back to raw ingest only when embeddings aren't ready. + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + { + let fallback_input = input.clone(); + match storage.smart_ingest(input) { + Ok(result) => { + return Ok(serde_json::json!({ + "success": true, + "nodeId": result.node.id, + "decision": result.decision, + "message": format!("Knowledge ingested successfully. Node ID: {} ({})", result.node.id, result.decision), + "hasEmbedding": result.node.has_embedding.unwrap_or(false), + "similarity": result.similarity, + "reason": result.reason, + })); + } + Err(_) => { + // smart_ingest failed — fall through to raw ingest with cloned input + let node = storage.ingest(fallback_input).map_err(|e| e.to_string())?; + return Ok(serde_json::json!({ + "success": true, + "nodeId": node.id, + "decision": "create", + "message": format!("Knowledge ingested successfully. Node ID: {}", node.id), + "hasEmbedding": node.has_embedding.unwrap_or(false), + })); + } + } + } + + // Fallback for builds without embedding features + #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] + { + let node = storage.ingest(input).map_err(|e| e.to_string())?; + Ok(serde_json::json!({ + "success": true, + "nodeId": node.id, + "decision": "create", + "message": format!("Knowledge ingested successfully. Node ID: {}", node.id), + "hasEmbedding": node.has_embedding.unwrap_or(false), + })) + } } // ============================================================================ diff --git a/packages/core/.gitignore b/packages/core/.gitignore deleted file mode 100644 index 9a540b7..0000000 --- a/packages/core/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# Dependencies -node_modules/ - -# Build output -dist/ - -# Database (user data) -*.db -*.db-wal -*.db-shm - -# Environment -.env -.env.local - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# Logs -*.log -npm-debug.log* - -# Test coverage -coverage/ - -# Temporary files -*.tmp -*.temp diff --git a/packages/core/README.md b/packages/core/README.md deleted file mode 100644 index 4da304b..0000000 --- a/packages/core/README.md +++ /dev/null @@ -1,186 +0,0 @@ -# Vestige - -[![npm version](https://img.shields.io/npm/v/vestige-mcp.svg)](https://www.npmjs.com/package/vestige-mcp) -[![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-blue.svg)](https://modelcontextprotocol.io) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -**Git Blame for AI Thoughts** - Memory that decays, strengthens, and discovers connections like the human mind. - -![Vestige Demo](./docs/assets/hero-demo.gif) - -## Why Vestige? - -| Feature | Vestige | Mem0 | Zep | Letta | -|---------|--------|------|-----|-------| -| FSRS-5 spaced repetition | Yes | No | No | No | -| Dual-strength memory | Yes | No | No | No | -| Sentiment-weighted retention | Yes | No | Yes | No | -| Local-first (no cloud) | Yes | No | No | No | -| Git context capture | Yes | No | No | No | -| Semantic connections | Yes | Limited | Yes | Yes | -| Free & open source | Yes | Freemium | Freemium | Yes | - -## Quickstart - -```bash -# Install -npx vestige-mcp init - -# Add to Claude Desktop config -# ~/.config/claude/claude_desktop_config.json (Mac/Linux) -# %APPDATA%\Claude\claude_desktop_config.json (Windows) -{ - "mcpServers": { - "vestige": { - "command": "npx", - "args": ["vestige-mcp"] - } - } -} - -# Restart Claude Desktop - done! -``` - -## Key Concepts - -### Cognitive Science Foundation - -Vestige implements proven memory science: - -- **FSRS-5**: State-of-the-art spaced repetition algorithm (powers Anki's 100M+ users) -- **Dual-Strength Memory**: Separate storage and retrieval strength (Bjork & Bjork, 1992) -- **Ebbinghaus Decay**: Memories fade naturally without reinforcement using `R = e^(-t/S)` -- **Sentiment Weighting**: Emotional memories decay slower via AFINN-165 lexicon analysis - -### Developer Features - -- **Git-Blame for Thoughts**: Every memory captures git branch, commit hash, and changed files -- **REM Cycle**: Background connection discovery between unrelated memories -- **Shadow Self**: Queue unsolved problems for future inspiration when new knowledge arrives - -## MCP Tools - -| Tool | Description | -|------|-------------| -| `ingest` | Store knowledge with metadata (source, people, tags, git context) | -| `recall` | Search memories by query with relevance ranking | -| `get_knowledge` | Retrieve specific memory by ID | -| `get_related` | Find connected nodes via graph traversal | -| `mark_reviewed` | Reinforce a memory (triggers spaced repetition) | -| `remember_person` | Add/update person in your network | -| `get_person` | Retrieve person details and relationship health | -| `daily_brief` | Get summary of memory state and review queue | -| `health_check` | Check database health with recommendations | -| `backup` | Create timestamped database backup | - -## MCP Resources - -| Resource | URI | Description | -|----------|-----|-------------| -| Recent memories | `memory://knowledge/recent` | Last 20 stored memories | -| Decaying memories | `memory://knowledge/decaying` | Memories below 50% retention | -| People network | `memory://people/network` | Your relationship graph | -| System context | `memory://context` | Active window, git branch, clipboard | - -## CLI Commands - -```bash -# Memory -vestige stats # Quick overview -vestige recall "query" # Search memories -vestige review # Show due for review - -# Ingestion -vestige eat # Ingest documentation - -# REM Cycle -vestige dream # Discover connections -vestige dream --dry-run # Preview only - -# Shadow Self -vestige problem "desc" # Log unsolved problem -vestige problems # List open problems -vestige solve "fix" # Mark solved - -# Context -vestige context # Show current context -vestige watch # Start context daemon - -# Maintenance -vestige backup # Create backup -vestige optimize # Vacuum and reindex -vestige decay # Apply memory decay -``` - -## Configuration - -Create `~/.vestige/config.json`: - -```json -{ - "fsrs": { - "desiredRetention": 0.9, - "maxStability": 365 - }, - "rem": { - "enabled": true, - "maxAnalyze": 50, - "minStrength": 0.3 - }, - "decay": { - "sentimentBoost": 2.0 - } -} -``` - -### Database Locations - -| File | Path | -|------|------| -| Main database | `~/.vestige/vestige.db` | -| Shadow Self | `~/.vestige/shadow.db` | -| Backups | `~/.vestige/backups/` | -| Context | `~/.vestige/context.json` | - -## How It Works - -### Memory Decay - -``` -Retention = e^(-days/stability) - -New memory: S=1.0 -> 37% after 1 day -Reviewed once: S=2.5 -> 67% after 1 day -Reviewed 3x: S=15.6 -> 94% after 1 day -Emotional: S x 1.85 boost -``` - -### REM Cycle Connections - -The REM cycle discovers hidden relationships: - -| Connection Type | Trigger | Strength | -|----------------|---------|----------| -| `entity_shared` | Same people mentioned | 0.5 + (count * 0.2) | -| `concept_overlap` | 2+ shared concepts | 0.4 + (count * 0.15) | -| `keyword_similarity` | Jaccard > 15% | similarity * 2 | -| `temporal_proximity` | Same day + overlap | 0.3 | - -## Documentation - -- [API Reference](./docs/api.md) - Full TypeScript API documentation -- [Configuration](./docs/configuration.md) - All config options -- [Architecture](./docs/architecture.md) - System design and data flow -- [Cognitive Science](./docs/cognitive-science.md) - The research behind Vestige - -## Contributing - -See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. - -## License - -MIT - see [LICENSE](./LICENSE) - ---- - -**Vestige**: The only AI memory system built on 130 years of cognitive science research. diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json deleted file mode 100644 index d1e5e6a..0000000 --- a/packages/core/package-lock.json +++ /dev/null @@ -1,6126 +0,0 @@ -{ - "name": "vestige-mcp", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "vestige-mcp", - "version": "0.1.0", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "better-sqlite3": "^11.0.0", - "chokidar": "^3.6.0", - "chromadb": "^1.9.0", - "date-fns": "^3.6.0", - "glob": "^10.4.0", - "gray-matter": "^4.0.3", - "marked": "^12.0.0", - "nanoid": "^5.0.7", - "natural": "^6.12.0", - "node-cron": "^3.0.3", - "ollama": "^0.5.0", - "p-limit": "^6.0.0", - "zod": "^3.23.0" - }, - "bin": { - "vestige": "dist/cli.js" - }, - "devDependencies": { - "@rstest/core": "^0.8.0", - "@types/better-sqlite3": "^7.6.10", - "@types/node": "^20.14.0", - "@types/node-cron": "^3.0.11", - "tsup": "^8.1.0", - "typescript": "^5.4.5", - "vitest": "^1.6.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", - "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@module-federation/error-codes": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", - "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@module-federation/runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", - "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/runtime-core": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@module-federation/runtime-core": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", - "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@module-federation/runtime-tools": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", - "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/webpack-bundler-runtime": "0.22.0" - } - }, - "node_modules/@module-federation/sdk": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", - "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", - "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", - "integrity": "sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@redis/bloom": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", - "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/client": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", - "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", - "license": "MIT", - "peer": true, - "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@redis/graph": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", - "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/json": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", - "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/search": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", - "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/time-series": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", - "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.3.tgz", - "integrity": "sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.3.tgz", - "integrity": "sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.3.tgz", - "integrity": "sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.3.tgz", - "integrity": "sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.3.tgz", - "integrity": "sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.3.tgz", - "integrity": "sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.3.tgz", - "integrity": "sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.3.tgz", - "integrity": "sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.3.tgz", - "integrity": "sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.3.tgz", - "integrity": "sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.3.tgz", - "integrity": "sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.3.tgz", - "integrity": "sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.3.tgz", - "integrity": "sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.3.tgz", - "integrity": "sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.3.tgz", - "integrity": "sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.3.tgz", - "integrity": "sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.3.tgz", - "integrity": "sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.3.tgz", - "integrity": "sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.3.tgz", - "integrity": "sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.3.tgz", - "integrity": "sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.3.tgz", - "integrity": "sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.3.tgz", - "integrity": "sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.3.tgz", - "integrity": "sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.3.tgz", - "integrity": "sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.3.tgz", - "integrity": "sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rsbuild/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.7.2.tgz", - "integrity": "sha512-VAFO6cM+cyg2ntxNW6g3tB2Jc5J5mpLjLluvm7VtW2uceNzyUlVv41o66Yp1t1ikxd3ljtqegViXem62JqzveA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rspack/core": "~1.7.1", - "@rspack/lite-tapable": "~1.1.0", - "@swc/helpers": "^0.5.18", - "core-js": "~3.47.0", - "jiti": "^2.6.1" - }, - "bin": { - "rsbuild": "bin/rsbuild.js" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/@rspack/binding": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.3.tgz", - "integrity": "sha512-N943pbPktJPymiYZWZMZMVX/PeSU42cWGpBly82N+ibNCX/Oo4yKWE0v+TyIJm5JaUFhtF2NpvzRbrjg/6skqw==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.7.3", - "@rspack/binding-darwin-x64": "1.7.3", - "@rspack/binding-linux-arm64-gnu": "1.7.3", - "@rspack/binding-linux-arm64-musl": "1.7.3", - "@rspack/binding-linux-x64-gnu": "1.7.3", - "@rspack/binding-linux-x64-musl": "1.7.3", - "@rspack/binding-wasm32-wasi": "1.7.3", - "@rspack/binding-win32-arm64-msvc": "1.7.3", - "@rspack/binding-win32-ia32-msvc": "1.7.3", - "@rspack/binding-win32-x64-msvc": "1.7.3" - } - }, - "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.3.tgz", - "integrity": "sha512-sXha3xG2KDkXLVjrmnw5kGhBriH2gFd9KAyD2ZBq0sH/gNIvqEaWhAFoO1YtrKU6rCgiSBrs0frfGc6DEqWfTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rspack/binding-darwin-x64": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.3.tgz", - "integrity": "sha512-AUWMBgaPo7NgpW7arlw9laj9ZQxg7EjC5pnSCRH4BVPV+8egdoPCn5DZk05M25m73crKnGl8c7CrwTRNZeaPrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.3.tgz", - "integrity": "sha512-SodEX3+1/GLz0LobX9cY1QdjJ1NftSEh4C2vGpr71iA3MS9HyXuw4giqSeRQ4DpCybqpdS/3RLjVqFQEfGpcnw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.3.tgz", - "integrity": "sha512-ydD2fNdEy+G7EYJ/a3FfdFZPfrLj/UnZocCNlZTTSHEhu+jURdQk0hwV11CvL+sjnKU5e/8IVMGUzhu3Gu8Ghg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.3.tgz", - "integrity": "sha512-adnDbUqafSAI6/N6vZ+iONSo1W3yUpnNtJqP3rVp7+YdABhUpbOhtaY37qpIJ3uFajXctYFyISPrb4MWl1M9Yg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.3.tgz", - "integrity": "sha512-5jnjdODk5HCUFPN6rTaFukynDU4Fn9eCL+4TSp6mqo6YAnfnJEuzDjfetA8t3aQFcAs7WriQfNwvdcA4HvYtbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-wasm32-wasi": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.3.tgz", - "integrity": "sha512-WLQK0ksUzMkVeGoHAMIxenmeEU5tMvFDK36Aip7VRj7T6vZTcAwvbMwc38QrIAvlG7dqWoxgPQi35ba1igNNDw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "1.0.7" - } - }, - "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.3.tgz", - "integrity": "sha512-RAetPeY45g2NW6fID46VTV7mwY4Lqyw/flLbvCG28yrVOSkekw1KMCr1k335O3VNeqD+5dZDi1n+mwiAx/KMmA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.3.tgz", - "integrity": "sha512-X3c1B609DxzW++FdWf7kkoXWwsC/DUEJ1N1qots4T0P2G2V+pDQfjdTRSC0YQ75toAvwZqpwGzToQJ9IwQ4Ayw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.3.tgz", - "integrity": "sha512-f6AvZbJGIg+7NggHXv0+lyMzvIUfeCxcB5DNbo3H5AalIgwkoFpcBXLBqgMVIbqA0yNyP06eiK98rpzc9ulQQg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.3.tgz", - "integrity": "sha512-GUiTRTz6+gbfM2g3ixXqrvPSeHmyAFu/qHEZZjbYFeDtZhpy1gVaVAHiZfaaIIm+vRlNi7JmULWFZQFKwpQB9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/runtime-tools": "0.22.0", - "@rspack/binding": "1.7.3", - "@rspack/lite-tapable": "1.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.1" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@rspack/lite-tapable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", - "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rstest/core": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@rstest/core/-/core-0.8.0.tgz", - "integrity": "sha512-zHpWPYN7T27YrtRwMM4dVm5PU1qQzAhX2ALspll1QT49BzuRHmJc2h3MaXTQ8F9k7sPMbhE+pGx9JQ7Vn7r+rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rsbuild/core": "1.7.2", - "@types/chai": "^5.2.3", - "tinypool": "^1.1.1" - }, - "bin": { - "rstest": "bin/rstest.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/@rstest/core/node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/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/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/chai/node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", - "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/node-cron": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", - "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/afinn-165": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/afinn-165/-/afinn-165-1.0.4.tgz", - "integrity": "sha512-7+Wlx3BImrK0HiG6y3lU4xX7SpBPSSu8T9iguPMlaueRFxjbYwAQrp9lqZUuFikqKbd/en8lVREILvP2J80uJA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/afinn-165-financialmarketnews": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/afinn-165-financialmarketnews/-/afinn-165-financialmarketnews-3.0.0.tgz", - "integrity": "sha512-0g9A1S3ZomFIGDTzZ0t6xmv4AuokBvBmpes8htiyHpH7N4xDmvSQL6UxL/Zcs2ypRb3VwgCscaD8Q3zEawKYhw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/apparatus": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/apparatus/-/apparatus-0.0.10.tgz", - "integrity": "sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg==", - "license": "MIT", - "dependencies": { - "sylvester": ">= 0.0.8" - }, - "engines": { - "node": ">=0.2.6" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/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/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/chromadb": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/chromadb/-/chromadb-1.10.5.tgz", - "integrity": "sha512-+IeTjjf44pKUY3vp1BacwO2tFAPcWCd64zxPZZm98dVj/kbSBeaHKB2D6eX7iRLHS1PTVASuqoR6mAJ+nrsTBg==", - "license": "Apache-2.0", - "dependencies": { - "cliui": "^8.0.1", - "isomorphic-fetch": "^3.0.0" - }, - "engines": { - "node": ">=14.17.0" - }, - "peerDependencies": { - "@google/generative-ai": "^0.1.1", - "cohere-ai": "^5.0.0 || ^6.0.0 || ^7.0.0", - "ollama": "^0.5.0", - "openai": "^3.0.0 || ^4.0.0", - "voyageai": "^0.0.3-1" - }, - "peerDependenciesMeta": { - "@google/generative-ai": { - "optional": true - }, - "cohere-ai": { - "optional": true - }, - "ollama": { - "optional": true - }, - "openai": { - "optional": true - }, - "voyageai": { - "optional": true - } - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-js": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", - "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/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/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", - "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/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/marked": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", - "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/memjs": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/memjs/-/memjs-1.3.2.tgz", - "integrity": "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mongodb": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", - "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.2" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.3.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.21.0.tgz", - "integrity": "sha512-dW2U01gN8EVQT5KAO5AkzjbqWc8A/CsEq15jOzq/M9ISpy8jw3iq7W9ZP135h9zykFOMt3AMxq4+anvt2YNJgw==", - "license": "MIT", - "dependencies": { - "bson": "^6.10.4", - "kareem": "2.6.3", - "mongodb": "~6.20.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "license": "MIT", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", - "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/natural": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/natural/-/natural-6.12.0.tgz", - "integrity": "sha512-ZV/cuaxOvJ7CSxQRYHc6nlx7ql6hVPQc20N5ubdqVbotWnnqsNc+0/QG+ACIC3XPQ4rfrQrdC/1k47v1cSszTQ==", - "license": "MIT", - "dependencies": { - "afinn-165": "^1.0.2", - "afinn-165-financialmarketnews": "^3.0.0", - "apparatus": "^0.0.10", - "dotenv": "^16.4.5", - "memjs": "^1.3.2", - "mongoose": "^8.2.0", - "pg": "^8.11.3", - "redis": "^4.6.13", - "safe-stable-stringify": "^2.2.0", - "stopwords-iso": "^1.1.0", - "sylvester": "^0.0.12", - "underscore": "^1.9.1", - "uuid": "^9.0.1", - "wordnet-db": "^3.1.11" - }, - "engines": { - "node": ">=0.4.10" - } - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.87.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", - "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-cron": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", - "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", - "license": "ISC", - "dependencies": { - "uuid": "8.3.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/node-cron/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ollama": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.18.tgz", - "integrity": "sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg==", - "license": "MIT", - "peer": true, - "dependencies": { - "whatwg-fetch": "^3.6.20" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", - "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/pg": { - "version": "8.17.2", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", - "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", - "license": "MIT", - "peer": true, - "dependencies": { - "pg-connection-string": "^2.10.1", - "pg-pool": "^3.11.0", - "pg-protocol": "^1.11.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.3.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", - "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz", - "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", - "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", - "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redis": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", - "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", - "license": "MIT", - "workspaces": [ - "./packages/*" - ], - "dependencies": { - "@redis/bloom": "1.2.0", - "@redis/client": "1.6.1", - "@redis/graph": "1.1.1", - "@redis/json": "1.0.7", - "@redis/search": "1.2.0", - "@redis/time-series": "1.1.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rollup": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz", - "integrity": "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==", - "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.55.3", - "@rollup/rollup-android-arm64": "4.55.3", - "@rollup/rollup-darwin-arm64": "4.55.3", - "@rollup/rollup-darwin-x64": "4.55.3", - "@rollup/rollup-freebsd-arm64": "4.55.3", - "@rollup/rollup-freebsd-x64": "4.55.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.3", - "@rollup/rollup-linux-arm-musleabihf": "4.55.3", - "@rollup/rollup-linux-arm64-gnu": "4.55.3", - "@rollup/rollup-linux-arm64-musl": "4.55.3", - "@rollup/rollup-linux-loong64-gnu": "4.55.3", - "@rollup/rollup-linux-loong64-musl": "4.55.3", - "@rollup/rollup-linux-ppc64-gnu": "4.55.3", - "@rollup/rollup-linux-ppc64-musl": "4.55.3", - "@rollup/rollup-linux-riscv64-gnu": "4.55.3", - "@rollup/rollup-linux-riscv64-musl": "4.55.3", - "@rollup/rollup-linux-s390x-gnu": "4.55.3", - "@rollup/rollup-linux-x64-gnu": "4.55.3", - "@rollup/rollup-linux-x64-musl": "4.55.3", - "@rollup/rollup-openbsd-x64": "4.55.3", - "@rollup/rollup-openharmony-arm64": "4.55.3", - "@rollup/rollup-win32-arm64-msvc": "4.55.3", - "@rollup/rollup-win32-ia32-msvc": "4.55.3", - "@rollup/rollup-win32-x64-gnu": "4.55.3", - "@rollup/rollup-win32-x64-msvc": "4.55.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", - "license": "MIT" - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/stopwords-iso": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stopwords-iso/-/stopwords-iso-1.1.0.tgz", - "integrity": "sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sylvester": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/sylvester/-/sylvester-0.0.12.tgz", - "integrity": "sha512-SzRP5LQ6Ts2G5NyAa/jg16s8e3R7rfdFjizy1zeoecYWw+nGL+YA1xZvW/+iJmidBGSdLkuvdwTYEyJEb+EiUw==", - "engines": { - "node": ">=0.2.6" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/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.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/tsup/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/tsup/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/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": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "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/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/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": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" - }, - "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": "1.6.1", - "@vitest/ui": "1.6.1", - "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/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/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/wordnet-db": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/wordnet-db/-/wordnet-db-3.1.14.tgz", - "integrity": "sha512-zVyFsvE+mq9MCmwXUWHIcpfbrHHClZWZiVOzKSxNJruIcFn2RbY55zkhiAMMxM8zCVSmtNiViq8FsAZSFpMYag==", - "license": "MIT", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } - } - } -} diff --git a/packages/core/package.json b/packages/core/package.json deleted file mode 100644 index 86c147b..0000000 --- a/packages/core/package.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "@vestige/core", - "version": "0.3.0", - "description": "Cognitive memory for AI - FSRS-5, dual-strength, sleep consolidation", - "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./fsrs": { - "types": "./dist/core/fsrs.d.ts", - "import": "./dist/core/fsrs.js" - }, - "./database": { - "types": "./dist/core/database.d.ts", - "import": "./dist/core/database.js" - } - }, - "bin": { - "vestige": "./dist/cli.js" - }, - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "start": "node dist/index.js", - "inspect": "npx @anthropic-ai/mcp-inspector node dist/index.js", - "test": "rstest", - "lint": "eslint src --ext .ts", - "typecheck": "tsc --noEmit" - }, - "keywords": [ - "mcp", - "memory", - "cognitive-science", - "fsrs", - "spaced-repetition", - "knowledge-management", - "second-brain", - "ai", - "claude" - ], - "author": "samvallad33", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "better-sqlite3": "^11.0.0", - "chokidar": "^3.6.0", - "chromadb": "^1.9.0", - "date-fns": "^3.6.0", - "glob": "^10.4.0", - "gray-matter": "^4.0.3", - "marked": "^12.0.0", - "nanoid": "^5.0.7", - "natural": "^6.12.0", - "node-cron": "^3.0.3", - "ollama": "^0.5.0", - "p-limit": "^6.0.0", - "zod": "^3.23.0" - }, - "devDependencies": { - "@rstest/core": "^0.8.0", - "@types/better-sqlite3": "^7.6.10", - "@types/node": "^20.14.0", - "@types/node-cron": "^3.0.11", - "tsup": "^8.1.0", - "typescript": "^5.4.5" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/packages/core/pnpm-lock.yaml b/packages/core/pnpm-lock.yaml deleted file mode 100644 index 27a530f..0000000 --- a/packages/core/pnpm-lock.yaml +++ /dev/null @@ -1,3920 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@modelcontextprotocol/sdk': - specifier: ^1.0.0 - version: 1.25.3(hono@4.11.5)(zod@3.25.76) - better-sqlite3: - specifier: ^11.0.0 - version: 11.10.0 - chokidar: - specifier: ^3.6.0 - version: 3.6.0 - chromadb: - specifier: ^1.9.0 - version: 1.10.5(ollama@0.5.18) - date-fns: - specifier: ^3.6.0 - version: 3.6.0 - glob: - specifier: ^10.4.0 - version: 10.5.0 - gray-matter: - specifier: ^4.0.3 - version: 4.0.3 - marked: - specifier: ^12.0.0 - version: 12.0.2 - nanoid: - specifier: ^5.0.7 - version: 5.1.6 - natural: - specifier: ^6.12.0 - version: 6.12.0 - node-cron: - specifier: ^3.0.3 - version: 3.0.3 - ollama: - specifier: ^0.5.0 - version: 0.5.18 - p-limit: - specifier: ^6.0.0 - version: 6.2.0 - zod: - specifier: ^3.23.0 - version: 3.25.76 - devDependencies: - '@rstest/core': - specifier: ^0.8.0 - version: 0.8.0 - '@types/better-sqlite3': - specifier: ^7.6.10 - version: 7.6.13 - '@types/node': - specifier: ^20.14.0 - version: 20.19.30 - '@types/node-cron': - specifier: ^3.0.11 - version: 3.0.11 - tsup: - specifier: ^8.1.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) - typescript: - specifier: ^5.4.5 - version: 5.9.3 - vitest: - specifier: ^1.6.0 - version: 1.6.1(@types/node@20.19.30) - -packages: - - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} - - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@hono/node-server@1.19.9': - resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@modelcontextprotocol/sdk@1.25.3': - resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - - '@module-federation/error-codes@0.22.0': - resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} - - '@module-federation/runtime-core@0.22.0': - resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} - - '@module-federation/runtime-tools@0.22.0': - resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} - - '@module-federation/runtime@0.22.0': - resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} - - '@module-federation/sdk@0.22.0': - resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} - - '@module-federation/webpack-bundler-runtime@0.22.0': - resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} - - '@mongodb-js/saslprep@1.4.5': - resolution: {integrity: sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==} - - '@napi-rs/wasm-runtime@1.0.7': - resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/client@1.6.1': - resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} - engines: {node: '>=14'} - - '@redis/graph@1.1.1': - resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/json@1.0.7': - resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/search@1.2.0': - resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/time-series@1.1.0': - resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@rollup/rollup-android-arm-eabi@4.56.0': - resolution: {integrity: sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.56.0': - resolution: {integrity: sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.56.0': - resolution: {integrity: sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.56.0': - resolution: {integrity: sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.56.0': - resolution: {integrity: sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.56.0': - resolution: {integrity: sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.56.0': - resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.56.0': - resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.56.0': - resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.56.0': - resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.56.0': - resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.56.0': - resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.56.0': - resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.56.0': - resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.56.0': - resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.56.0': - resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.56.0': - resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.56.0': - resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.56.0': - resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openbsd-x64@4.56.0': - resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.56.0': - resolution: {integrity: sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.56.0': - resolution: {integrity: sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.56.0': - resolution: {integrity: sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.56.0': - resolution: {integrity: sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.56.0': - resolution: {integrity: sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==} - cpu: [x64] - os: [win32] - - '@rsbuild/core@1.7.2': - resolution: {integrity: sha512-VAFO6cM+cyg2ntxNW6g3tB2Jc5J5mpLjLluvm7VtW2uceNzyUlVv41o66Yp1t1ikxd3ljtqegViXem62JqzveA==} - engines: {node: '>=18.12.0'} - hasBin: true - - '@rspack/binding-darwin-arm64@1.7.3': - resolution: {integrity: sha512-sXha3xG2KDkXLVjrmnw5kGhBriH2gFd9KAyD2ZBq0sH/gNIvqEaWhAFoO1YtrKU6rCgiSBrs0frfGc6DEqWfTA==} - cpu: [arm64] - os: [darwin] - - '@rspack/binding-darwin-x64@1.7.3': - resolution: {integrity: sha512-AUWMBgaPo7NgpW7arlw9laj9ZQxg7EjC5pnSCRH4BVPV+8egdoPCn5DZk05M25m73crKnGl8c7CrwTRNZeaPrw==} - cpu: [x64] - os: [darwin] - - '@rspack/binding-linux-arm64-gnu@1.7.3': - resolution: {integrity: sha512-SodEX3+1/GLz0LobX9cY1QdjJ1NftSEh4C2vGpr71iA3MS9HyXuw4giqSeRQ4DpCybqpdS/3RLjVqFQEfGpcnw==} - cpu: [arm64] - os: [linux] - - '@rspack/binding-linux-arm64-musl@1.7.3': - resolution: {integrity: sha512-ydD2fNdEy+G7EYJ/a3FfdFZPfrLj/UnZocCNlZTTSHEhu+jURdQk0hwV11CvL+sjnKU5e/8IVMGUzhu3Gu8Ghg==} - cpu: [arm64] - os: [linux] - - '@rspack/binding-linux-x64-gnu@1.7.3': - resolution: {integrity: sha512-adnDbUqafSAI6/N6vZ+iONSo1W3yUpnNtJqP3rVp7+YdABhUpbOhtaY37qpIJ3uFajXctYFyISPrb4MWl1M9Yg==} - cpu: [x64] - os: [linux] - - '@rspack/binding-linux-x64-musl@1.7.3': - resolution: {integrity: sha512-5jnjdODk5HCUFPN6rTaFukynDU4Fn9eCL+4TSp6mqo6YAnfnJEuzDjfetA8t3aQFcAs7WriQfNwvdcA4HvYtbA==} - cpu: [x64] - os: [linux] - - '@rspack/binding-wasm32-wasi@1.7.3': - resolution: {integrity: sha512-WLQK0ksUzMkVeGoHAMIxenmeEU5tMvFDK36Aip7VRj7T6vZTcAwvbMwc38QrIAvlG7dqWoxgPQi35ba1igNNDw==} - cpu: [wasm32] - - '@rspack/binding-win32-arm64-msvc@1.7.3': - resolution: {integrity: sha512-RAetPeY45g2NW6fID46VTV7mwY4Lqyw/flLbvCG28yrVOSkekw1KMCr1k335O3VNeqD+5dZDi1n+mwiAx/KMmA==} - cpu: [arm64] - os: [win32] - - '@rspack/binding-win32-ia32-msvc@1.7.3': - resolution: {integrity: sha512-X3c1B609DxzW++FdWf7kkoXWwsC/DUEJ1N1qots4T0P2G2V+pDQfjdTRSC0YQ75toAvwZqpwGzToQJ9IwQ4Ayw==} - cpu: [ia32] - os: [win32] - - '@rspack/binding-win32-x64-msvc@1.7.3': - resolution: {integrity: sha512-f6AvZbJGIg+7NggHXv0+lyMzvIUfeCxcB5DNbo3H5AalIgwkoFpcBXLBqgMVIbqA0yNyP06eiK98rpzc9ulQQg==} - cpu: [x64] - os: [win32] - - '@rspack/binding@1.7.3': - resolution: {integrity: sha512-N943pbPktJPymiYZWZMZMVX/PeSU42cWGpBly82N+ibNCX/Oo4yKWE0v+TyIJm5JaUFhtF2NpvzRbrjg/6skqw==} - - '@rspack/core@1.7.3': - resolution: {integrity: sha512-GUiTRTz6+gbfM2g3ixXqrvPSeHmyAFu/qHEZZjbYFeDtZhpy1gVaVAHiZfaaIIm+vRlNi7JmULWFZQFKwpQB9Q==} - engines: {node: '>=18.12.0'} - peerDependencies: - '@swc/helpers': '>=0.5.1' - peerDependenciesMeta: - '@swc/helpers': - optional: true - - '@rspack/lite-tapable@1.1.0': - resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} - - '@rstest/core@0.8.0': - resolution: {integrity: sha512-zHpWPYN7T27YrtRwMM4dVm5PU1qQzAhX2ALspll1QT49BzuRHmJc2h3MaXTQ8F9k7sPMbhE+pGx9JQ7Vn7r+rQ==} - engines: {node: '>=18.12.0'} - hasBin: true - peerDependencies: - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - happy-dom: - optional: true - jsdom: - optional: true - - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - - '@swc/helpers@0.5.18': - resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} - - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/better-sqlite3@7.6.13': - resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/node-cron@3.0.11': - resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} - - '@types/node@20.19.30': - resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} - - '@types/webidl-conversions@7.0.3': - resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} - - '@types/whatwg-url@11.0.5': - resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} - - '@vitest/expect@1.6.1': - resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} - - '@vitest/runner@1.6.1': - resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} - - '@vitest/snapshot@1.6.1': - resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} - - '@vitest/spy@1.6.1': - resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} - - '@vitest/utils@1.6.1': - resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} - - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - afinn-165-financialmarketnews@3.0.0: - resolution: {integrity: sha512-0g9A1S3ZomFIGDTzZ0t6xmv4AuokBvBmpes8htiyHpH7N4xDmvSQL6UxL/Zcs2ypRb3VwgCscaD8Q3zEawKYhw==} - - afinn-165@1.0.4: - resolution: {integrity: sha512-7+Wlx3BImrK0HiG6y3lU4xX7SpBPSSu8T9iguPMlaueRFxjbYwAQrp9lqZUuFikqKbd/en8lVREILvP2J80uJA==} - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - apparatus@0.0.10: - resolution: {integrity: sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg==} - engines: {node: '>=0.2.6'} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - better-sqlite3@11.10.0: - resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - bson@6.10.4: - resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} - engines: {node: '>=16.20.1'} - - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} - - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - - chromadb@1.10.5: - resolution: {integrity: sha512-+IeTjjf44pKUY3vp1BacwO2tFAPcWCd64zxPZZm98dVj/kbSBeaHKB2D6eX7iRLHS1PTVASuqoR6mAJ+nrsTBg==} - engines: {node: '>=14.17.0'} - peerDependencies: - '@google/generative-ai': ^0.1.1 - cohere-ai: ^5.0.0 || ^6.0.0 || ^7.0.0 - ollama: ^0.5.0 - openai: ^3.0.0 || ^4.0.0 - voyageai: ^0.0.3-1 - peerDependenciesMeta: - '@google/generative-ai': - optional: true - cohere-ai: - optional: true - ollama: - optional: true - openai: - optional: true - voyageai: - optional: true - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - - content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - core-js@3.47.0: - resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - date-fns@3.6.0: - resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} - engines: {node: '>=6'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} - engines: {node: '>=18'} - hasBin: true - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - hasBin: true - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hono@4.11.5: - resolution: {integrity: sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g==} - engines: {node: '>=16.9.0'} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isomorphic-fetch@3.0.0: - resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} - hasBin: true - - jose@6.1.3: - resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - - kareem@2.6.3: - resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} - engines: {node: '>=12.0.0'} - - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} - engines: {node: '>=14'} - - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - marked@12.0.2: - resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==} - engines: {node: '>= 18'} - hasBin: true - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - memjs@1.3.2: - resolution: {integrity: sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg==} - engines: {node: '>=0.10.0'} - - memory-pager@1.5.0: - resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - - mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - - mongodb-connection-string-url@3.0.2: - resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} - - mongodb@6.20.0: - resolution: {integrity: sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==} - engines: {node: '>=16.20.1'} - peerDependencies: - '@aws-sdk/credential-providers': ^3.188.0 - '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 - gcp-metadata: ^5.2.0 - kerberos: ^2.0.1 - mongodb-client-encryption: '>=6.0.0 <7' - snappy: ^7.3.2 - socks: ^2.7.1 - peerDependenciesMeta: - '@aws-sdk/credential-providers': - optional: true - '@mongodb-js/zstd': - optional: true - gcp-metadata: - optional: true - kerberos: - optional: true - mongodb-client-encryption: - optional: true - snappy: - optional: true - socks: - optional: true - - mongoose@8.21.1: - resolution: {integrity: sha512-1LhrVeHwiyAGxwSaYSq2uf32izQD+qoM2c8wq63W8MIsJBxKQDBnMkhJct55m0qqCsm2Maq8aPpIIfOHSYAqxg==} - engines: {node: '>=16.20.1'} - - mpath@0.9.0: - resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} - engines: {node: '>=4.0.0'} - - mquery@5.0.0: - resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} - engines: {node: '>=14.0.0'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@5.1.6: - resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} - engines: {node: ^18 || >=20} - hasBin: true - - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - - natural@6.12.0: - resolution: {integrity: sha512-ZV/cuaxOvJ7CSxQRYHc6nlx7ql6hVPQc20N5ubdqVbotWnnqsNc+0/QG+ACIC3XPQ4rfrQrdC/1k47v1cSszTQ==} - engines: {node: '>=0.4.10'} - - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - node-abi@3.87.0: - resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} - engines: {node: '>=10'} - - node-cron@3.0.3: - resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} - engines: {node: '>=6.0.0'} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - ollama@0.5.18: - resolution: {integrity: sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg==} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - - p-limit@6.2.0: - resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} - engines: {node: '>=18'} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - - pg-cloudflare@1.3.0: - resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} - - pg-connection-string@2.10.1: - resolution: {integrity: sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==} - - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-pool@3.11.0: - resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} - peerDependencies: - pg: '>=8.0' - - pg-protocol@1.11.0: - resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - - pg@8.17.2: - resolution: {integrity: sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==} - engines: {node: '>= 16.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - - pgpass@1.0.5: - resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-bytea@1.0.1: - resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - hasBin: true - - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - qs@6.14.1: - resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} - engines: {node: '>=0.6'} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - redis@4.7.1: - resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - rollup@4.56.0: - resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - sift@17.1.3: - resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - - sparse-bitfield@3.0.3: - resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} - - split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - stopwords-iso@1.1.0: - resolution: {integrity: sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg==} - engines: {node: '>=0.10.0'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - - strip-literal@2.1.1: - resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} - - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - sylvester@0.0.12: - resolution: {integrity: sha512-SzRP5LQ6Ts2G5NyAa/jg16s8e3R7rfdFjizy1zeoecYWw+nGL+YA1xZvW/+iJmidBGSdLkuvdwTYEyJEb+EiUw==} - engines: {node: '>=0.2.6'} - - tar-fs@2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} - engines: {node: '>=14.0.0'} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - - type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} - - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - - underscore@1.13.7: - resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vite-node@1.6.1: - resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - 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 - - vitest@1.6.1: - resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.1 - '@vitest/ui': 1.6.1 - 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 - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - - whatwg-fetch@3.6.20: - resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} - - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - wordnet-db@3.1.14: - resolution: {integrity: sha512-zVyFsvE+mq9MCmwXUWHIcpfbrHHClZWZiVOzKSxNJruIcFn2RbY55zkhiAMMxM8zCVSmtNiViq8FsAZSFpMYag==} - engines: {node: '>=0.6.0'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - - yocto-queue@1.2.2: - resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} - engines: {node: '>=12.20'} - - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} - peerDependencies: - zod: ^3.25 || ^4 - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - -snapshots: - - '@emnapi/core@1.8.1': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.8.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.1.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/aix-ppc64@0.27.2': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.27.2': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-arm@0.27.2': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/android-x64@0.27.2': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.27.2': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.27.2': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.27.2': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.27.2': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.27.2': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-arm@0.27.2': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.27.2': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.27.2': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.27.2': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.27.2': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.27.2': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.27.2': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/linux-x64@0.27.2': - optional: true - - '@esbuild/netbsd-arm64@0.27.2': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.27.2': - optional: true - - '@esbuild/openbsd-arm64@0.27.2': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.27.2': - optional: true - - '@esbuild/openharmony-arm64@0.27.2': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.27.2': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.27.2': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.27.2': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - - '@esbuild/win32-x64@0.27.2': - optional: true - - '@hono/node-server@1.19.9(hono@4.11.5)': - dependencies: - hono: 4.11.5 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@modelcontextprotocol/sdk@1.25.3(hono@4.11.5)(zod@3.25.76)': - dependencies: - '@hono/node-server': 1.19.9(hono@4.11.5) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) - jose: 6.1.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - transitivePeerDependencies: - - hono - - supports-color - - '@module-federation/error-codes@0.22.0': {} - - '@module-federation/runtime-core@0.22.0': - dependencies: - '@module-federation/error-codes': 0.22.0 - '@module-federation/sdk': 0.22.0 - - '@module-federation/runtime-tools@0.22.0': - dependencies: - '@module-federation/runtime': 0.22.0 - '@module-federation/webpack-bundler-runtime': 0.22.0 - - '@module-federation/runtime@0.22.0': - dependencies: - '@module-federation/error-codes': 0.22.0 - '@module-federation/runtime-core': 0.22.0 - '@module-federation/sdk': 0.22.0 - - '@module-federation/sdk@0.22.0': {} - - '@module-federation/webpack-bundler-runtime@0.22.0': - dependencies: - '@module-federation/runtime': 0.22.0 - '@module-federation/sdk': 0.22.0 - - '@mongodb-js/saslprep@1.4.5': - dependencies: - sparse-bitfield: 3.0.3 - - '@napi-rs/wasm-runtime@1.0.7': - dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@redis/bloom@1.2.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/client@1.6.1': - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - - '@redis/graph@1.1.1(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/json@1.0.7(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/search@1.2.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/time-series@1.1.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@rollup/rollup-android-arm-eabi@4.56.0': - optional: true - - '@rollup/rollup-android-arm64@4.56.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.56.0': - optional: true - - '@rollup/rollup-darwin-x64@4.56.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.56.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.56.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.56.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.56.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.56.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.56.0': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.56.0': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.56.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.56.0': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.56.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.56.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.56.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.56.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.56.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.56.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.56.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.56.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.56.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.56.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.56.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.56.0': - optional: true - - '@rsbuild/core@1.7.2': - dependencies: - '@rspack/core': 1.7.3(@swc/helpers@0.5.18) - '@rspack/lite-tapable': 1.1.0 - '@swc/helpers': 0.5.18 - core-js: 3.47.0 - jiti: 2.6.1 - - '@rspack/binding-darwin-arm64@1.7.3': - optional: true - - '@rspack/binding-darwin-x64@1.7.3': - optional: true - - '@rspack/binding-linux-arm64-gnu@1.7.3': - optional: true - - '@rspack/binding-linux-arm64-musl@1.7.3': - optional: true - - '@rspack/binding-linux-x64-gnu@1.7.3': - optional: true - - '@rspack/binding-linux-x64-musl@1.7.3': - optional: true - - '@rspack/binding-wasm32-wasi@1.7.3': - dependencies: - '@napi-rs/wasm-runtime': 1.0.7 - optional: true - - '@rspack/binding-win32-arm64-msvc@1.7.3': - optional: true - - '@rspack/binding-win32-ia32-msvc@1.7.3': - optional: true - - '@rspack/binding-win32-x64-msvc@1.7.3': - optional: true - - '@rspack/binding@1.7.3': - optionalDependencies: - '@rspack/binding-darwin-arm64': 1.7.3 - '@rspack/binding-darwin-x64': 1.7.3 - '@rspack/binding-linux-arm64-gnu': 1.7.3 - '@rspack/binding-linux-arm64-musl': 1.7.3 - '@rspack/binding-linux-x64-gnu': 1.7.3 - '@rspack/binding-linux-x64-musl': 1.7.3 - '@rspack/binding-wasm32-wasi': 1.7.3 - '@rspack/binding-win32-arm64-msvc': 1.7.3 - '@rspack/binding-win32-ia32-msvc': 1.7.3 - '@rspack/binding-win32-x64-msvc': 1.7.3 - - '@rspack/core@1.7.3(@swc/helpers@0.5.18)': - dependencies: - '@module-federation/runtime-tools': 0.22.0 - '@rspack/binding': 1.7.3 - '@rspack/lite-tapable': 1.1.0 - optionalDependencies: - '@swc/helpers': 0.5.18 - - '@rspack/lite-tapable@1.1.0': {} - - '@rstest/core@0.8.0': - dependencies: - '@rsbuild/core': 1.7.2 - '@types/chai': 5.2.3 - tinypool: 1.1.1 - - '@sinclair/typebox@0.27.8': {} - - '@swc/helpers@0.5.18': - dependencies: - tslib: 2.8.1 - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/better-sqlite3@7.6.13': - dependencies: - '@types/node': 20.19.30 - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.8': {} - - '@types/node-cron@3.0.11': {} - - '@types/node@20.19.30': - dependencies: - undici-types: 6.21.0 - - '@types/webidl-conversions@7.0.3': {} - - '@types/whatwg-url@11.0.5': - dependencies: - '@types/webidl-conversions': 7.0.3 - - '@vitest/expect@1.6.1': - dependencies: - '@vitest/spy': 1.6.1 - '@vitest/utils': 1.6.1 - chai: 4.5.0 - - '@vitest/runner@1.6.1': - dependencies: - '@vitest/utils': 1.6.1 - p-limit: 5.0.0 - pathe: 1.1.2 - - '@vitest/snapshot@1.6.1': - dependencies: - magic-string: 0.30.21 - pathe: 1.1.2 - pretty-format: 29.7.0 - - '@vitest/spy@1.6.1': - dependencies: - tinyspy: 2.2.1 - - '@vitest/utils@1.6.1': - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - - acorn-walk@8.3.4: - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - afinn-165-financialmarketnews@3.0.0: {} - - afinn-165@1.0.4: {} - - ajv-formats@3.0.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - ansi-styles@6.2.3: {} - - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - apparatus@0.0.10: - dependencies: - sylvester: 0.0.12 - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - assertion-error@1.1.0: {} - - assertion-error@2.0.1: {} - - balanced-match@1.0.2: {} - - base64-js@1.5.1: {} - - better-sqlite3@11.10.0: - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - - binary-extensions@2.3.0: {} - - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - - body-parser@2.2.2: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.14.1 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - bson@6.10.4: {} - - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - bundle-require@5.1.0(esbuild@0.27.2): - dependencies: - esbuild: 0.27.2 - load-tsconfig: 0.2.5 - - bytes@3.1.2: {} - - cac@6.7.14: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - chai@4.5.0: - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.1.0 - - check-error@1.0.3: - dependencies: - get-func-name: 2.0.2 - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - chownr@1.1.4: {} - - chromadb@1.10.5(ollama@0.5.18): - dependencies: - cliui: 8.0.1 - isomorphic-fetch: 3.0.0 - optionalDependencies: - ollama: 0.5.18 - transitivePeerDependencies: - - encoding - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - cluster-key-slot@1.1.2: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - commander@4.1.1: {} - - confbox@0.1.8: {} - - consola@3.4.2: {} - - content-disposition@1.0.1: {} - - content-type@1.0.5: {} - - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - core-js@3.47.0: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - date-fns@3.6.0: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - deep-eql@4.1.4: - dependencies: - type-detect: 4.1.0 - - deep-extend@0.6.0: {} - - depd@2.0.0: {} - - detect-libc@2.1.2: {} - - diff-sequences@29.6.3: {} - - dotenv@16.6.1: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - eastasianwidth@0.2.0: {} - - ee-first@1.1.1: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - encodeurl@2.0.0: {} - - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - esbuild@0.21.5: - 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 - - esbuild@0.27.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 - - escape-html@1.0.3: {} - - esprima@4.0.1: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - etag@1.8.1: {} - - eventsource-parser@3.0.6: {} - - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.0.6 - - execa@8.0.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - - expand-template@2.0.3: {} - - express-rate-limit@7.5.1(express@5.2.1): - dependencies: - express: 5.2.1 - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.2 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.1 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - - fast-deep-equal@3.1.3: {} - - fast-uri@3.1.0: {} - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - file-uri-to-path@1.0.0: {} - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.0 - rollup: 4.56.0 - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - forwarded@0.2.0: {} - - fresh@2.0.0: {} - - fs-constants@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - generic-pool@3.9.0: {} - - get-func-name@2.0.2: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@8.0.1: {} - - github-from-package@0.0.0: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - gopd@1.2.0: {} - - gray-matter@4.0.3: - dependencies: - js-yaml: 3.14.2 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - - has-symbols@1.1.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hono@4.11.5: {} - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - human-signals@5.0.0: {} - - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - - ieee754@1.2.1: {} - - inherits@2.0.4: {} - - ini@1.3.8: {} - - ipaddr.js@1.9.1: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-extendable@0.1.1: {} - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - is-promise@4.0.0: {} - - is-stream@3.0.0: {} - - isexe@2.0.0: {} - - isomorphic-fetch@3.0.0: - dependencies: - node-fetch: 2.7.0 - whatwg-fetch: 3.6.20 - transitivePeerDependencies: - - encoding - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jiti@2.6.1: {} - - jose@6.1.3: {} - - joycon@3.1.1: {} - - js-tokens@9.0.1: {} - - js-yaml@3.14.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - json-schema-traverse@1.0.0: {} - - json-schema-typed@8.0.2: {} - - kareem@2.6.3: {} - - kind-of@6.0.3: {} - - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - - local-pkg@0.5.1: - dependencies: - mlly: 1.8.0 - pkg-types: 1.3.1 - - loupe@2.3.7: - dependencies: - get-func-name: 2.0.2 - - lru-cache@10.4.3: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - marked@12.0.2: {} - - math-intrinsics@1.1.0: {} - - media-typer@1.1.0: {} - - memjs@1.3.2: {} - - memory-pager@1.5.0: {} - - merge-descriptors@2.0.0: {} - - merge-stream@2.0.0: {} - - mime-db@1.54.0: {} - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - - mimic-fn@4.0.0: {} - - mimic-response@3.1.0: {} - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - minimist@1.2.8: {} - - minipass@7.1.2: {} - - mkdirp-classic@0.5.3: {} - - mlly@1.8.0: - dependencies: - acorn: 8.15.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - - mongodb-connection-string-url@3.0.2: - dependencies: - '@types/whatwg-url': 11.0.5 - whatwg-url: 14.2.0 - - mongodb@6.20.0: - dependencies: - '@mongodb-js/saslprep': 1.4.5 - bson: 6.10.4 - mongodb-connection-string-url: 3.0.2 - - mongoose@8.21.1: - dependencies: - bson: 6.10.4 - kareem: 2.6.3 - mongodb: 6.20.0 - mpath: 0.9.0 - mquery: 5.0.0 - ms: 2.1.3 - sift: 17.1.3 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks - - supports-color - - mpath@0.9.0: {} - - mquery@5.0.0: - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - ms@2.1.3: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.11: {} - - nanoid@5.1.6: {} - - napi-build-utils@2.0.0: {} - - natural@6.12.0: - dependencies: - afinn-165: 1.0.4 - afinn-165-financialmarketnews: 3.0.0 - apparatus: 0.0.10 - dotenv: 16.6.1 - memjs: 1.3.2 - mongoose: 8.21.1 - pg: 8.17.2 - redis: 4.7.1 - safe-stable-stringify: 2.5.0 - stopwords-iso: 1.1.0 - sylvester: 0.0.12 - underscore: 1.13.7 - uuid: 9.0.1 - wordnet-db: 3.1.14 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - pg-native - - snappy - - socks - - supports-color - - negotiator@1.0.0: {} - - node-abi@3.87.0: - dependencies: - semver: 7.7.3 - - node-cron@3.0.3: - dependencies: - uuid: 8.3.2 - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - normalize-path@3.0.0: {} - - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - ollama@0.5.18: - dependencies: - whatwg-fetch: 3.6.20 - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@6.0.0: - dependencies: - mimic-fn: 4.0.0 - - p-limit@5.0.0: - dependencies: - yocto-queue: 1.2.2 - - p-limit@6.2.0: - dependencies: - yocto-queue: 1.2.2 - - package-json-from-dist@1.0.1: {} - - parseurl@1.3.3: {} - - path-key@3.1.1: {} - - path-key@4.0.0: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - path-to-regexp@8.3.0: {} - - pathe@1.1.2: {} - - pathe@2.0.3: {} - - pathval@1.1.1: {} - - pg-cloudflare@1.3.0: - optional: true - - pg-connection-string@2.10.1: {} - - pg-int8@1.0.1: {} - - pg-pool@3.11.0(pg@8.17.2): - dependencies: - pg: 8.17.2 - - pg-protocol@1.11.0: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.1 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - pg@8.17.2: - dependencies: - pg-connection-string: 2.10.1 - pg-pool: 3.11.0(pg@8.17.2) - pg-protocol: 1.11.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.3.0 - - pgpass@1.0.5: - dependencies: - split2: 4.2.0 - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.3: {} - - pirates@4.0.7: {} - - pkce-challenge@5.0.1: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.0 - pathe: 2.0.3 - - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 2.6.1 - postcss: 8.5.6 - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postgres-array@2.0.0: {} - - postgres-bytea@1.0.1: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.87.0 - pump: 3.0.3 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.4 - tunnel-agent: 0.6.0 - - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - pump@3.0.3: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - - punycode@2.3.1: {} - - qs@6.14.1: - dependencies: - side-channel: 1.1.0 - - range-parser@1.2.1: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - - react-is@18.3.1: {} - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - readdirp@4.1.2: {} - - redis@4.7.1: - dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.6.1) - '@redis/client': 1.6.1 - '@redis/graph': 1.1.1(@redis/client@1.6.1) - '@redis/json': 1.0.7(@redis/client@1.6.1) - '@redis/search': 1.2.0(@redis/client@1.6.1) - '@redis/time-series': 1.1.0(@redis/client@1.6.1) - - require-from-string@2.0.2: {} - - resolve-from@5.0.0: {} - - rollup@4.56.0: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.56.0 - '@rollup/rollup-android-arm64': 4.56.0 - '@rollup/rollup-darwin-arm64': 4.56.0 - '@rollup/rollup-darwin-x64': 4.56.0 - '@rollup/rollup-freebsd-arm64': 4.56.0 - '@rollup/rollup-freebsd-x64': 4.56.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.56.0 - '@rollup/rollup-linux-arm-musleabihf': 4.56.0 - '@rollup/rollup-linux-arm64-gnu': 4.56.0 - '@rollup/rollup-linux-arm64-musl': 4.56.0 - '@rollup/rollup-linux-loong64-gnu': 4.56.0 - '@rollup/rollup-linux-loong64-musl': 4.56.0 - '@rollup/rollup-linux-ppc64-gnu': 4.56.0 - '@rollup/rollup-linux-ppc64-musl': 4.56.0 - '@rollup/rollup-linux-riscv64-gnu': 4.56.0 - '@rollup/rollup-linux-riscv64-musl': 4.56.0 - '@rollup/rollup-linux-s390x-gnu': 4.56.0 - '@rollup/rollup-linux-x64-gnu': 4.56.0 - '@rollup/rollup-linux-x64-musl': 4.56.0 - '@rollup/rollup-openbsd-x64': 4.56.0 - '@rollup/rollup-openharmony-arm64': 4.56.0 - '@rollup/rollup-win32-arm64-msvc': 4.56.0 - '@rollup/rollup-win32-ia32-msvc': 4.56.0 - '@rollup/rollup-win32-x64-gnu': 4.56.0 - '@rollup/rollup-win32-x64-msvc': 4.56.0 - fsevents: 2.3.3 - - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.3.0 - transitivePeerDependencies: - - supports-color - - safe-buffer@5.2.1: {} - - safe-stable-stringify@2.5.0: {} - - safer-buffer@2.1.2: {} - - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - - semver@7.7.3: {} - - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - sift@17.1.3: {} - - siginfo@2.0.0: {} - - signal-exit@4.1.0: {} - - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - - source-map-js@1.2.1: {} - - source-map@0.7.6: {} - - sparse-bitfield@3.0.3: - dependencies: - memory-pager: 1.5.0 - - split2@4.2.0: {} - - sprintf-js@1.0.3: {} - - stackback@0.0.2: {} - - statuses@2.0.2: {} - - std-env@3.10.0: {} - - stopwords-iso@1.1.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - - strip-bom-string@1.0.0: {} - - strip-final-newline@3.0.0: {} - - strip-json-comments@2.0.1: {} - - strip-literal@2.1.1: - dependencies: - js-tokens: 9.0.1 - - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.15 - ts-interface-checker: 0.1.13 - - sylvester@0.0.12: {} - - tar-fs@2.1.4: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.3 - tar-stream: 2.2.0 - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - tinypool@0.8.4: {} - - tinypool@1.1.1: {} - - tinyspy@2.2.1: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - - tr46@0.0.3: {} - - tr46@5.1.1: - dependencies: - punycode: 2.3.1 - - tree-kill@1.2.2: {} - - ts-interface-checker@0.1.13: {} - - tslib@2.8.1: {} - - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.2) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.2 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6) - resolve-from: 5.0.0 - rollup: 4.56.0 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.6 - typescript: 5.9.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - - type-detect@4.1.0: {} - - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - - typescript@5.9.3: {} - - ufo@1.6.3: {} - - underscore@1.13.7: {} - - undici-types@6.21.0: {} - - unpipe@1.0.0: {} - - util-deprecate@1.0.2: {} - - uuid@8.3.2: {} - - uuid@9.0.1: {} - - vary@1.1.2: {} - - vite-node@1.6.1(@types/node@20.19.30): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - pathe: 1.1.2 - picocolors: 1.1.1 - vite: 5.4.21(@types/node@20.19.30) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite@5.4.21(@types/node@20.19.30): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.56.0 - optionalDependencies: - '@types/node': 20.19.30 - fsevents: 2.3.3 - - vitest@1.6.1(@types/node@20.19.30): - dependencies: - '@vitest/expect': 1.6.1 - '@vitest/runner': 1.6.1 - '@vitest/snapshot': 1.6.1 - '@vitest/spy': 1.6.1 - '@vitest/utils': 1.6.1 - acorn-walk: 8.3.4 - chai: 4.5.0 - debug: 4.4.3 - execa: 8.0.1 - local-pkg: 0.5.1 - magic-string: 0.30.21 - pathe: 1.1.2 - picocolors: 1.1.1 - std-env: 3.10.0 - strip-literal: 2.1.1 - tinybench: 2.9.0 - tinypool: 0.8.4 - vite: 5.4.21(@types/node@20.19.30) - vite-node: 1.6.1(@types/node@20.19.30) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.19.30 - transitivePeerDependencies: - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - webidl-conversions@3.0.1: {} - - webidl-conversions@7.0.0: {} - - whatwg-fetch@3.6.20: {} - - whatwg-url@14.2.0: - dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - wordnet-db@3.1.14: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - - wrappy@1.0.2: {} - - xtend@4.0.2: {} - - yallist@4.0.0: {} - - yocto-queue@1.2.2: {} - - zod-to-json-schema@3.25.1(zod@3.25.76): - dependencies: - zod: 3.25.76 - - zod@3.25.76: {} diff --git a/packages/core/rstest.config.ts b/packages/core/rstest.config.ts deleted file mode 100644 index 7bb14c3..0000000 --- a/packages/core/rstest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from '@rstest/core'; - -export default defineConfig({ - testMatch: ['**/*.test.ts'], - setupFiles: ['./src/__tests__/setup.ts'], - coverage: { - include: ['src/**/*.ts'], - exclude: ['src/__tests__/**', 'src/**/*.d.ts'], - }, -}); diff --git a/packages/core/src/__tests__/core/dual-strength.test.ts b/packages/core/src/__tests__/core/dual-strength.test.ts deleted file mode 100644 index 7d55731..0000000 --- a/packages/core/src/__tests__/core/dual-strength.test.ts +++ /dev/null @@ -1,1035 +0,0 @@ -/** - * Dual-Strength Memory Model Tests - * - * Tests for the Bjork & Bjork (1992) dual-strength memory model implementation. - * - * The model distinguishes between: - * - Storage strength: How well encoded a memory is (never decreases, only grows) - * - Retrieval strength: How accessible the memory is now (decays over time) - * - * Key insight: Difficult retrievals (when retrieval strength is low) increase - * storage strength MORE than easy retrievals (desirable difficulty principle). - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import Database from 'better-sqlite3'; -import { initializeDatabase, VestigeDatabase, analyzeSentimentIntensity } from '../../core/database.js'; -import path from 'path'; -import fs from 'fs'; -import os from 'os'; - -// ============================================================================ -// TEST UTILITIES -// ============================================================================ - -/** - * Create a test database in a temporary location - */ -function createTestDatabase(): Database.Database { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vestige-test-')); - const dbPath = path.join(tempDir, 'test.db'); - return initializeDatabase(dbPath); -} - -/** - * Create a test VestigeDatabase instance - */ -function createTestVestigeDatabase(): { db: VestigeDatabase; path: string } { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vestige-test-')); - const dbPath = path.join(tempDir, 'test.db'); - const db = new VestigeDatabase(dbPath); - return { db, path: dbPath }; -} - -/** - * Clean up test database - */ -function cleanupTestDatabase(db: Database.Database): void { - try { - db.close(); - } catch { - // Ignore close errors - } -} - -/** - * Clean up VestigeDatabase and its files - */ -function cleanupVestigeDatabase(db: VestigeDatabase, dbPath: string): void { - try { - db.close(); - // Clean up temp directory - const tempDir = path.dirname(dbPath); - fs.rmSync(tempDir, { recursive: true, force: true }); - } catch { - // Ignore cleanup errors - } -} - -/** - * Helper to insert a test node with specific properties - */ -function insertTestNode( - db: Database.Database, - overrides: Partial<{ - id: string; - storage_strength: number; - retrieval_strength: number; - retention_strength: number; - stability_factor: number; - sentiment_intensity: number; - last_accessed_at: string; - access_count: number; - review_count: number; - }> = {} -): string { - const id = overrides.id || `test-node-${Date.now()}-${Math.random().toString(36).slice(2)}`; - const now = new Date().toISOString(); - - const stmt = db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, summary, - created_at, updated_at, last_accessed_at, access_count, - retention_strength, stability_factor, sentiment_intensity, - storage_strength, retrieval_strength, review_count, - source_type, source_platform, confidence, tags - ) VALUES ( - ?, ?, ?, - ?, ?, ?, ?, - ?, ?, ?, - ?, ?, ?, - ?, ?, ?, ? - ) - `); - - stmt.run( - id, - 'Test content for dual-strength memory model', - 'Test summary', - now, - now, - overrides.last_accessed_at || now, - overrides.access_count ?? 0, - overrides.retention_strength ?? 1.0, - overrides.stability_factor ?? 1.0, - overrides.sentiment_intensity ?? 0, - overrides.storage_strength ?? 1.0, - overrides.retrieval_strength ?? 1.0, - overrides.review_count ?? 0, - 'note', - 'manual', - 0.8, - '[]' - ); - - return id; -} - -/** - * Helper to get node strengths from database - */ -function getNodeStrengths(db: Database.Database, id: string): { - storage_strength: number; - retrieval_strength: number; - retention_strength: number; - stability_factor: number; - sentiment_intensity: number; - access_count: number; - review_count: number; -} | null { - const stmt = db.prepare(` - SELECT storage_strength, retrieval_strength, retention_strength, - stability_factor, sentiment_intensity, access_count, review_count - FROM knowledge_nodes WHERE id = ? - `); - const row = stmt.get(id) as Record | undefined; - if (!row) return null; - return { - storage_strength: row['storage_strength'] as number, - retrieval_strength: row['retrieval_strength'] as number, - retention_strength: row['retention_strength'] as number, - stability_factor: row['stability_factor'] as number, - sentiment_intensity: row['sentiment_intensity'] as number, - access_count: row['access_count'] as number, - review_count: row['review_count'] as number, - }; -} - -/** - * Backdate a node's last_accessed_at by a number of days - */ -function backdateNode(db: Database.Database, id: string, daysAgo: number): void { - const pastDate = new Date(); - pastDate.setDate(pastDate.getDate() - daysAgo); - const stmt = db.prepare(`UPDATE knowledge_nodes SET last_accessed_at = ? WHERE id = ?`); - stmt.run(pastDate.toISOString(), id); -} - -// ============================================================================ -// STORAGE STRENGTH TESTS -// ============================================================================ - -describe('Dual-Strength Memory Model', () => { - describe('Storage Strength', () => { - let testDb: { db: VestigeDatabase; path: string }; - - beforeEach(() => { - testDb = createTestVestigeDatabase(); - }); - - afterEach(() => { - cleanupVestigeDatabase(testDb.db, testDb.path); - }); - - it('should start at 1.0 for new nodes', () => { - // Create a new node - const insertedNode = testDb.db.insertNode({ - content: 'New knowledge to remember', - sourceType: 'note', - sourcePlatform: 'manual', - }); - - // Fetch the node to get full data (insertNode returns partial data) - const node = testDb.db.getNode(insertedNode.id); - - // Verify storage_strength is 1.0 - expect(node?.storageStrength).toBe(1.0); - }); - - it('should increase by 0.05 on regular access', () => { - // Create a node - const insertedNode = testDb.db.insertNode({ - content: 'Memory to access', - sourceType: 'note', - sourcePlatform: 'manual', - }); - - // Fetch to get full data - const node = testDb.db.getNode(insertedNode.id); - const initialStorage = node?.storageStrength ?? 1.0; - - // Access the node - testDb.db.updateNodeAccess(insertedNode.id); - - // Retrieve updated node - const updatedNode = testDb.db.getNode(insertedNode.id); - - // Storage should increase by 0.05 - expect(updatedNode?.storageStrength).toBeCloseTo(initialStorage + 0.05, 4); - }); - - it('should increase by 0.1 on successful review (easy recall)', () => { - // Create a node with retention above lapse threshold (0.3) - const insertedNode = testDb.db.insertNode({ - content: 'Memory for easy review', - sourceType: 'note', - sourcePlatform: 'manual', - retentionStrength: 0.5, // Above lapse threshold - }); - - // Fetch to get full data - const node = testDb.db.getNode(insertedNode.id); - const initialStorage = node?.storageStrength ?? 1.0; - - // Mark as reviewed (successful recall) - testDb.db.markReviewed(insertedNode.id); - - // Retrieve updated node - const updatedNode = testDb.db.getNode(insertedNode.id); - - // Storage should increase by 0.1 for successful recall - expect(updatedNode?.storageStrength).toBeCloseTo(initialStorage + 0.1, 4); - }); - - it('should increase by 0.3 on difficult review (lapse recall) - desirable difficulty', () => { - // Create a node with retention below lapse threshold (0.3) - const insertedNode = testDb.db.insertNode({ - content: 'Memory for difficult review', - sourceType: 'note', - sourcePlatform: 'manual', - retentionStrength: 0.2, // Below lapse threshold - forgot it - }); - - // Fetch to get full data - const node = testDb.db.getNode(insertedNode.id); - const initialStorage = node?.storageStrength ?? 1.0; - - // Mark as reviewed (lapse - difficult recall) - testDb.db.markReviewed(insertedNode.id); - - // Retrieve updated node - const updatedNode = testDb.db.getNode(insertedNode.id); - - // Storage should increase by 0.3 for difficult recall (desirable difficulty) - expect(updatedNode?.storageStrength).toBeCloseTo(initialStorage + 0.3, 4); - }); - - it('should NEVER decrease (critical invariant)', () => { - // Create a node with high storage strength - const insertedNode = testDb.db.insertNode({ - content: 'Memory with high storage', - sourceType: 'note', - sourcePlatform: 'manual', - storageStrength: 5.0, - }); - - // Apply decay - testDb.db.applyDecay(); - - // Retrieve updated node - const updatedNode = testDb.db.getNode(insertedNode.id); - - // Storage strength should NOT decrease - expect(updatedNode?.storageStrength).toBeGreaterThanOrEqual(5.0); - }); - - it('should accumulate across multiple accesses', () => { - // Create a node - const insertedNode = testDb.db.insertNode({ - content: 'Memory to access multiple times', - sourceType: 'note', - sourcePlatform: 'manual', - }); - - // Access 10 times - for (let i = 0; i < 10; i++) { - testDb.db.updateNodeAccess(insertedNode.id); - } - - // Retrieve updated node - const updatedNode = testDb.db.getNode(insertedNode.id); - - // Storage should have increased by 0.05 * 10 = 0.5 - expect(updatedNode?.storageStrength).toBeCloseTo(1.0 + 0.5, 4); - }); - - it('should be able to exceed 1.0 (unbounded growth)', () => { - // Create a node and access it many times - const insertedNode = testDb.db.insertNode({ - content: 'Memory to access many times', - sourceType: 'note', - sourcePlatform: 'manual', - }); - - // Access 50 times (0.05 * 50 = 2.5) - for (let i = 0; i < 50; i++) { - testDb.db.updateNodeAccess(insertedNode.id); - } - - // Retrieve updated node - const updatedNode = testDb.db.getNode(insertedNode.id); - - // Storage should be well above 1.0 - expect(updatedNode?.storageStrength).toBeGreaterThan(3.0); - }); - - it('should preserve storage strength after decay is applied', () => { - // Get raw database and backdate - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 10.0, - retrieval_strength: 1.0, - retention_strength: 1.0, - }); - backdateNode(rawDb, id, 30); // 30 days ago - - // Apply decay manually (simulating the applyDecay method logic) - // Note: We're testing that storage_strength doesn't change after decay - const now = Date.now(); - const nodes = rawDb.prepare(` - SELECT id, last_accessed_at, retention_strength, stability_factor, sentiment_intensity, - storage_strength, retrieval_strength - FROM knowledge_nodes WHERE id = ? - `).all(id) as { - id: string; - last_accessed_at: string; - storage_strength: number; - }[]; - - for (const n of nodes) { - const lastAccessed = new Date(n.last_accessed_at).getTime(); - const daysSince = (now - lastAccessed) / (1000 * 60 * 60 * 24); - - // Storage strength should NOT change in decay - expect(daysSince).toBeGreaterThan(0); - expect(n.storage_strength).toBe(10.0); - } - - cleanupTestDatabase(rawDb); - }); - }); - - // ============================================================================ - // RETRIEVAL STRENGTH TESTS - // ============================================================================ - - describe('Retrieval Strength', () => { - let testDb: { db: VestigeDatabase; path: string }; - - beforeEach(() => { - testDb = createTestVestigeDatabase(); - }); - - afterEach(() => { - cleanupVestigeDatabase(testDb.db, testDb.path); - }); - - it('should start at 1.0 for new nodes', () => { - const insertedNode = testDb.db.insertNode({ - content: 'Fresh memory', - sourceType: 'note', - sourcePlatform: 'manual', - }); - - // Fetch to get full data - const node = testDb.db.getNode(insertedNode.id); - - expect(node?.retrievalStrength).toBe(1.0); - }); - - it('should decay over time following power law', () => { - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 1.0, - retrieval_strength: 1.0, - sentiment_intensity: 0, - }); - - // Backdate by 7 days - backdateNode(rawDb, id, 7); - - const before = getNodeStrengths(rawDb, id); - expect(before?.retrieval_strength).toBe(1.0); - - // Simulate decay calculation - const now = Date.now(); - const lastAccessed = new Date(); - lastAccessed.setDate(lastAccessed.getDate() - 7); - const daysSince = 7; - const storageStrength = 1.0; - const sentimentIntensity = 0; - - // effectiveDecayRate = 1 / (storageStrength * (1 + sentimentIntensity)) - const effectiveDecayRate = 1 / (storageStrength * (1 + sentimentIntensity)); - const expectedRetrieval = Math.max(0.1, Math.exp(-daysSince * effectiveDecayRate)); - - // After 7 days with storage=1.0, sentiment=0 - // decay rate = 1 / (1 * 1) = 1 - // retrieval = exp(-7 * 1) = exp(-7) ~= 0.00091 - // But clamped to 0.1 minimum - expect(expectedRetrieval).toBe(0.1); - - cleanupTestDatabase(rawDb); - }); - - it('should reset to 1.0 on access', () => { - // Create a node with decayed retrieval - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 5.0, - retrieval_strength: 0.3, // Already decayed - sentiment_intensity: 0, - }); - - const before = getNodeStrengths(rawDb, id); - expect(before?.retrieval_strength).toBe(0.3); - - // Update access (using raw SQL to simulate updateNodeAccess) - rawDb.prepare(` - UPDATE knowledge_nodes - SET last_accessed_at = ?, - access_count = access_count + 1, - storage_strength = storage_strength + 0.05, - retrieval_strength = 1.0 - WHERE id = ? - `).run(new Date().toISOString(), id); - - const after = getNodeStrengths(rawDb, id); - expect(after?.retrieval_strength).toBe(1.0); - - cleanupTestDatabase(rawDb); - }); - - it('should decay slower with higher storage strength', () => { - // Test with two nodes: one with low storage, one with high storage - const rawDb = createTestDatabase(); - - // Create low storage node - const lowStorageId = insertTestNode(rawDb, { - id: 'low-storage-node', - storage_strength: 1.0, - retrieval_strength: 1.0, - sentiment_intensity: 0, - }); - - // Create high storage node - const highStorageId = insertTestNode(rawDb, { - id: 'high-storage-node', - storage_strength: 10.0, - retrieval_strength: 1.0, - sentiment_intensity: 0, - }); - - // Backdate both by 3 days - backdateNode(rawDb, lowStorageId, 3); - backdateNode(rawDb, highStorageId, 3); - - // Calculate expected decay for each - const daysSince = 3; - - // Low storage: decay rate = 1 / (1 * 1) = 1 - // retrieval = exp(-3 * 1) = exp(-3) ~= 0.05 - const lowStorageDecay = Math.max(0.1, Math.exp(-daysSince * (1 / (1.0 * 1)))); - - // High storage: decay rate = 1 / (10 * 1) = 0.1 - // retrieval = exp(-3 * 0.1) = exp(-0.3) ~= 0.74 - const highStorageDecay = Math.max(0.1, Math.exp(-daysSince * (1 / (10.0 * 1)))); - - // High storage node should retain more - expect(highStorageDecay).toBeGreaterThan(lowStorageDecay); - expect(highStorageDecay).toBeGreaterThan(0.7); - expect(lowStorageDecay).toBeLessThan(0.2); - - cleanupTestDatabase(rawDb); - }); - - it('should decay slower with higher sentiment intensity', () => { - // Test emotional vs neutral memory - const rawDb = createTestDatabase(); - - // Create neutral node - const neutralId = insertTestNode(rawDb, { - id: 'neutral-node', - storage_strength: 1.0, - retrieval_strength: 1.0, - sentiment_intensity: 0, - }); - - // Create emotional node - const emotionalId = insertTestNode(rawDb, { - id: 'emotional-node', - storage_strength: 1.0, - retrieval_strength: 1.0, - sentiment_intensity: 1.0, // Highly emotional - }); - - // Both have same storage, but different sentiment - const daysSince = 3; - - // Neutral: decay rate = 1 / (1 * (1 + 0)) = 1 - const neutralDecay = Math.max(0.1, Math.exp(-daysSince * (1 / (1.0 * (1 + 0))))); - - // Emotional: decay rate = 1 / (1 * (1 + 1)) = 0.5 - const emotionalDecay = Math.max(0.1, Math.exp(-daysSince * (1 / (1.0 * (1 + 1))))); - - // Emotional memory should retain more - expect(emotionalDecay).toBeGreaterThan(neutralDecay); - - cleanupTestDatabase(rawDb); - }); - - it('should have minimum floor of 0.1 (never completely forgotten)', () => { - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 1.0, - retrieval_strength: 1.0, - sentiment_intensity: 0, - }); - - // Backdate by 365 days (a year) - backdateNode(rawDb, id, 365); - - const daysSince = 365; - const decayRate = 1 / (1.0 * 1); - const rawRetrieval = Math.exp(-daysSince * decayRate); - - // Should be clamped to 0.1 - const clampedRetrieval = Math.max(0.1, rawRetrieval); - expect(clampedRetrieval).toBe(0.1); - - cleanupTestDatabase(rawDb); - }); - - it('should reset to 1.0 on review', () => { - const insertedNode = testDb.db.insertNode({ - content: 'Memory to review', - sourceType: 'note', - sourcePlatform: 'manual', - retrievalStrength: 0.4, // Already somewhat decayed - }); - - // Mark as reviewed - testDb.db.markReviewed(insertedNode.id); - - // Get updated node - const updated = testDb.db.getNode(insertedNode.id); - - expect(updated?.retrievalStrength).toBe(1.0); - }); - }); - - // ============================================================================ - // COMBINED RETENTION STRENGTH TESTS - // ============================================================================ - - describe('Combined Retention Strength (backward compatible)', () => { - let rawDb: Database.Database; - - beforeEach(() => { - rawDb = createTestDatabase(); - }); - - afterEach(() => { - cleanupTestDatabase(rawDb); - }); - - it('should be computed from both strengths using weighted formula', () => { - // retention = (retrieval * 0.7) + (min(1, storage/10) * 0.3) - const storage = 5.0; - const retrieval = 0.8; - - const normalizedStorage = Math.min(1, storage / 10); - const expectedRetention = (retrieval * 0.7) + (normalizedStorage * 0.3); - - expect(expectedRetention).toBeCloseTo((0.8 * 0.7) + (0.5 * 0.3), 4); - expect(expectedRetention).toBeCloseTo(0.71, 2); - }); - - it('should be within [0, 1] range', () => { - // Test extreme values - const testCases = [ - { storage: 0, retrieval: 0 }, - { storage: 1, retrieval: 0 }, - { storage: 0, retrieval: 1 }, - { storage: 1, retrieval: 1 }, - { storage: 100, retrieval: 1 }, // Very high storage - { storage: 100, retrieval: 0.1 }, // High storage, low retrieval - ]; - - for (const tc of testCases) { - const normalizedStorage = Math.min(1, tc.storage / 10); - const retention = (tc.retrieval * 0.7) + (normalizedStorage * 0.3); - - expect(retention).toBeGreaterThanOrEqual(0); - expect(retention).toBeLessThanOrEqual(1); - } - }); - - it('should weight retrieval more heavily (70%)', () => { - // With same values, retrieval contribution should be larger - const storage = 10.0; // Normalized to 1.0 - const retrieval = 1.0; - - const normalizedStorage = Math.min(1, storage / 10); - const retention = (retrieval * 0.7) + (normalizedStorage * 0.3); - - const retrievalContribution = retrieval * 0.7; - const storageContribution = normalizedStorage * 0.3; - - expect(retrievalContribution).toBeGreaterThan(storageContribution); - expect(retrievalContribution).toBe(0.7); - expect(storageContribution).toBe(0.3); - }); - - it('should cap storage contribution at normalized value of 1', () => { - // Storage > 10 should not increase contribution - const storageValues = [10, 20, 50, 100]; - const retrieval = 0.5; - - let previousRetention: number | null = null; - - for (const storage of storageValues) { - const normalizedStorage = Math.min(1, storage / 10); - const retention = (retrieval * 0.7) + (normalizedStorage * 0.3); - - if (storage >= 10) { - // All should have same normalized storage contribution - expect(normalizedStorage).toBe(1); - if (previousRetention !== null) { - expect(retention).toBe(previousRetention); - } - } - previousRetention = retention; - } - }); - }); - - // ============================================================================ - // DATABASE INTEGRATION TESTS - // ============================================================================ - - describe('Database Integration', () => { - let rawDb: Database.Database; - - beforeEach(() => { - rawDb = createTestDatabase(); - }); - - afterEach(() => { - cleanupTestDatabase(rawDb); - }); - - it('should have dual-strength columns after migration', () => { - const columns = rawDb.prepare("PRAGMA table_info(knowledge_nodes)").all() as { name: string }[]; - const columnNames = columns.map(c => c.name); - - expect(columnNames).toContain('storage_strength'); - expect(columnNames).toContain('retrieval_strength'); - }); - - it('should insert with correct default values', () => { - const id = insertTestNode(rawDb); - const strengths = getNodeStrengths(rawDb, id); - - expect(strengths?.storage_strength).toBe(1.0); - expect(strengths?.retrieval_strength).toBe(1.0); - expect(strengths?.retention_strength).toBe(1.0); - }); - - it('should allow custom initial values', () => { - const id = insertTestNode(rawDb, { - storage_strength: 5.0, - retrieval_strength: 0.8, - retention_strength: 0.9, - }); - const strengths = getNodeStrengths(rawDb, id); - - expect(strengths?.storage_strength).toBe(5.0); - expect(strengths?.retrieval_strength).toBe(0.8); - expect(strengths?.retention_strength).toBe(0.9); - }); - - it('should update storage correctly on access', () => { - const id = insertTestNode(rawDb, { storage_strength: 1.0 }); - - // Simulate updateNodeAccess - rawDb.prepare(` - UPDATE knowledge_nodes - SET storage_strength = storage_strength + 0.05, - retrieval_strength = 1.0 - WHERE id = ? - `).run(id); - - const strengths = getNodeStrengths(rawDb, id); - expect(strengths?.storage_strength).toBeCloseTo(1.05, 4); - }); - - it('should update retrieval correctly during decay', () => { - const id = insertTestNode(rawDb, { - storage_strength: 2.0, - retrieval_strength: 1.0, - sentiment_intensity: 0, - }); - - // Backdate - backdateNode(rawDb, id, 1); - - // Simulate decay calculation - const daysSince = 1; - const storage = 2.0; - const sentiment = 0; - const decayRate = 1 / (storage * (1 + sentiment)); - const newRetrieval = Math.max(0.1, Math.exp(-daysSince * decayRate)); - - // Update - rawDb.prepare(` - UPDATE knowledge_nodes SET retrieval_strength = ? WHERE id = ? - `).run(newRetrieval, id); - - const strengths = getNodeStrengths(rawDb, id); - expect(strengths?.retrieval_strength).toBeCloseTo(newRetrieval, 4); - }); - }); - - // ============================================================================ - // EDGE CASES - // ============================================================================ - - describe('Edge Cases', () => { - let testDb: { db: VestigeDatabase; path: string }; - - beforeEach(() => { - testDb = createTestVestigeDatabase(); - }); - - afterEach(() => { - cleanupVestigeDatabase(testDb.db, testDb.path); - }); - - it('should handle new node with no accesses correctly', () => { - const insertedNode = testDb.db.insertNode({ - content: 'Brand new memory', - sourceType: 'note', - sourcePlatform: 'manual', - }); - - // Fetch to get full data - const node = testDb.db.getNode(insertedNode.id); - - expect(node?.storageStrength).toBe(1.0); - expect(node?.retrievalStrength).toBe(1.0); - expect(node?.accessCount).toBe(0); - expect(node?.reviewCount).toBe(0); - }); - - it('should handle heavily accessed node (storage >> 10)', () => { - // Create node with very high storage strength - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 50.0, // Very heavily accessed - retrieval_strength: 0.5, - sentiment_intensity: 0.5, - }); - - // Verify storage is preserved and high - const strengths = getNodeStrengths(rawDb, id); - expect(strengths?.storage_strength).toBe(50.0); - - // Decay should still work but be very slow - const daysSince = 1; - const storage = 50.0; - const sentiment = 0.5; - const decayRate = 1 / (storage * (1 + sentiment)); - const newRetrieval = Math.max(0.1, Math.exp(-daysSince * decayRate)); - - // With storage=50 and sentiment=0.5, decay rate = 1/(50*1.5) = 0.0133 - // retrieval after 1 day = exp(-0.0133) ~= 0.987 - expect(decayRate).toBeCloseTo(0.0133, 3); - expect(newRetrieval).toBeGreaterThan(0.98); - - cleanupTestDatabase(rawDb); - }); - - it('should handle long-decayed node (retrieval near 0.1 floor)', () => { - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 1.0, - retrieval_strength: 0.1, // Already at floor - sentiment_intensity: 0, - }); - - // Backdate further - backdateNode(rawDb, id, 100); - - // Retrieval should stay at floor - const daysSince = 100; - const decayRate = 1 / (1.0 * 1); - const rawRetrieval = Math.exp(-daysSince * decayRate); - const clampedRetrieval = Math.max(0.1, rawRetrieval); - - expect(rawRetrieval).toBeLessThan(0.001); - expect(clampedRetrieval).toBe(0.1); - - cleanupTestDatabase(rawDb); - }); - - it('should handle high sentiment emotional memory', () => { - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 1.0, - retrieval_strength: 1.0, - sentiment_intensity: 1.0, // Maximum emotional intensity - }); - - // Backdate by 7 days - backdateNode(rawDb, id, 7); - - // Calculate decay with high sentiment - const daysSince = 7; - const storage = 1.0; - const sentiment = 1.0; - const decayRate = 1 / (storage * (1 + sentiment)); // 1 / (1 * 2) = 0.5 - const newRetrieval = Math.max(0.1, Math.exp(-daysSince * decayRate)); - - // With sentiment=1.0, decay is halved - // retrieval = exp(-7 * 0.5) = exp(-3.5) ~= 0.03 - // But clamped to 0.1 - expect(decayRate).toBe(0.5); - expect(newRetrieval).toBe(0.1); // Clamped - - // Compare to neutral: would be exp(-7) ~= 0.0009, also clamped to 0.1 - cleanupTestDatabase(rawDb); - }); - - it('should handle combined high storage and high sentiment', () => { - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 10.0, - retrieval_strength: 1.0, - sentiment_intensity: 1.0, - }); - - // Calculate decay - const daysSince = 30; // A month - const storage = 10.0; - const sentiment = 1.0; - const decayRate = 1 / (storage * (1 + sentiment)); // 1 / (10 * 2) = 0.05 - const newRetrieval = Math.max(0.1, Math.exp(-daysSince * decayRate)); - - // retrieval = exp(-30 * 0.05) = exp(-1.5) ~= 0.22 - expect(decayRate).toBe(0.05); - expect(newRetrieval).toBeGreaterThan(0.2); - expect(newRetrieval).toBeLessThan(0.25); - - cleanupTestDatabase(rawDb); - }); - - it('should handle zero sentiment correctly', () => { - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 5.0, - retrieval_strength: 1.0, - sentiment_intensity: 0, // Neutral - }); - - const daysSince = 5; - const storage = 5.0; - const sentiment = 0; - const decayRate = 1 / (storage * (1 + sentiment)); // 1 / (5 * 1) = 0.2 - const newRetrieval = Math.max(0.1, Math.exp(-daysSince * decayRate)); - - // retrieval = exp(-5 * 0.2) = exp(-1) ~= 0.37 - expect(decayRate).toBe(0.2); - expect(newRetrieval).toBeGreaterThan(0.35); - expect(newRetrieval).toBeLessThan(0.4); - - cleanupTestDatabase(rawDb); - }); - }); - - // ============================================================================ - // SENTIMENT INTENSITY ANALYSIS TESTS - // ============================================================================ - - describe('Sentiment Intensity Analysis', () => { - it('should return 0 for neutral content', () => { - const content = 'The meeting is scheduled for 3pm tomorrow.'; - const intensity = analyzeSentimentIntensity(content); - expect(intensity).toBeLessThan(0.2); - }); - - it('should return high intensity for highly positive content', () => { - const content = 'I am absolutely thrilled! This is amazing and wonderful! Best day ever!'; - const intensity = analyzeSentimentIntensity(content); - expect(intensity).toBeGreaterThan(0.3); - }); - - it('should return high intensity for highly negative content', () => { - const content = 'I am completely devastated and heartbroken. This is terrible and awful!'; - const intensity = analyzeSentimentIntensity(content); - expect(intensity).toBeGreaterThan(0.3); - }); - - it('should measure intensity not polarity (both positive and negative should be high)', () => { - const positive = 'Absolutely fantastic! Amazing! Wonderful!'; - const negative = 'Terrible! Horrible! Devastating!'; - - const positiveIntensity = analyzeSentimentIntensity(positive); - const negativeIntensity = analyzeSentimentIntensity(negative); - - // Both should be emotionally intense - expect(positiveIntensity).toBeGreaterThan(0.2); - expect(negativeIntensity).toBeGreaterThan(0.2); - }); - - it('should handle empty content', () => { - const intensity = analyzeSentimentIntensity(''); - expect(intensity).toBe(0); - }); - - it('should be bounded between 0 and 1', () => { - const testCases = [ - '', - 'Hello', - 'Amazing wonderful fantastic brilliant extraordinary phenomenal', - 'Terrible horrible awful dreadful catastrophic disastrous devastating', - 'a'.repeat(1000), // Long neutral content - ]; - - for (const content of testCases) { - const intensity = analyzeSentimentIntensity(content); - expect(intensity).toBeGreaterThanOrEqual(0); - expect(intensity).toBeLessThanOrEqual(1); - } - }); - }); - - // ============================================================================ - // DESIRABLE DIFFICULTY TESTS - // ============================================================================ - - describe('Desirable Difficulty Principle', () => { - let testDb: { db: VestigeDatabase; path: string }; - - beforeEach(() => { - testDb = createTestVestigeDatabase(); - }); - - afterEach(() => { - cleanupVestigeDatabase(testDb.db, testDb.path); - }); - - it('should reward difficult recalls with higher storage increase', () => { - // Create two nodes: one for easy recall, one for difficult - const easyInserted = testDb.db.insertNode({ - content: 'Easy recall memory', - sourceType: 'note', - sourcePlatform: 'manual', - retentionStrength: 0.8, // Above lapse threshold - easy - }); - - const hardInserted = testDb.db.insertNode({ - content: 'Difficult recall memory', - sourceType: 'note', - sourcePlatform: 'manual', - retentionStrength: 0.2, // Below lapse threshold - forgot it - }); - - // Fetch to get full data - const easyNode = testDb.db.getNode(easyInserted.id); - const hardNode = testDb.db.getNode(hardInserted.id); - - const easyInitialStorage = easyNode?.storageStrength ?? 1.0; - const hardInitialStorage = hardNode?.storageStrength ?? 1.0; - - // Both reviewed - testDb.db.markReviewed(easyInserted.id); - testDb.db.markReviewed(hardInserted.id); - - const easyAfter = testDb.db.getNode(easyInserted.id); - const hardAfter = testDb.db.getNode(hardInserted.id); - - const easyIncrease = (easyAfter?.storageStrength ?? 0) - easyInitialStorage; - const hardIncrease = (hardAfter?.storageStrength ?? 0) - hardInitialStorage; - - // Difficult recall should increase storage MORE - expect(hardIncrease).toBeGreaterThan(easyIncrease); - expect(easyIncrease).toBeCloseTo(0.1, 2); // Easy: +0.1 - expect(hardIncrease).toBeCloseTo(0.3, 2); // Hard: +0.3 - }); - - it('should reset stability on lapse but still increase storage', () => { - // Create a node with high stability that then lapses using raw database - const rawDb = createTestDatabase(); - const id = insertTestNode(rawDb, { - storage_strength: 3.0, - retrieval_strength: 0.2, - retention_strength: 0.2, - stability_factor: 10.0, // High stability from previous reviews - }); - - const before = getNodeStrengths(rawDb, id); - expect(before?.stability_factor).toBe(10.0); - expect(before?.storage_strength).toBe(3.0); - - // Now when reviewed (lapsed), stability should reset but storage should increase - // This is the key insight: even failures strengthen encoding (desirable difficulty) - - // Verify the invariants we expect from the model - // Storage should never decrease regardless of lapse - expect(before?.storage_strength).toBeGreaterThanOrEqual(1.0); - - cleanupTestDatabase(rawDb); - }); - }); -}); diff --git a/packages/core/src/__tests__/core/fsrs.test.ts b/packages/core/src/__tests__/core/fsrs.test.ts deleted file mode 100644 index 9022b1f..0000000 --- a/packages/core/src/__tests__/core/fsrs.test.ts +++ /dev/null @@ -1,1031 +0,0 @@ -/** - * Comprehensive tests for FSRS-5 (Free Spaced Repetition Scheduler) Algorithm - * - * Tests cover: - * - Initial difficulty and stability calculations - * - Retrievability decay over time - * - Difficulty updates with mean reversion - * - Stability growth/decay after reviews - * - Interval calculations - * - Full review flow scenarios - * - Sentiment boost functionality - * - Edge cases and boundary conditions - */ - -import { describe, it, expect, beforeEach } from '@rstest/core'; -import { - FSRSScheduler, - Grade, - FSRS_WEIGHTS, - FSRS_CONSTANTS, - initialDifficulty, - initialStability, - retrievability, - nextDifficulty, - nextRecallStability, - nextForgetStability, - nextInterval, - applySentimentBoost, - serializeFSRSState, - deserializeFSRSState, - optimalReviewTime, - isReviewDue, - type FSRSState, - type ReviewGrade, - type LearningState, -} from '../../core/fsrs.js'; - -describe('FSRS-5 Algorithm', () => { - let scheduler: FSRSScheduler; - - beforeEach(() => { - scheduler = new FSRSScheduler(); - }); - - // ========================================================================== - // 1. INITIAL DIFFICULTY TESTS - // ========================================================================== - - describe('initialDifficulty', () => { - it('should return highest difficulty for Again grade', () => { - const d = initialDifficulty(Grade.Again); - // With default weights: w4 - e^(w5*(1-1)) + 1 = 7.1949 - 1 + 1 = 7.1949 - expect(d).toBeCloseTo(7.19, 1); - }); - - it('should return lower difficulty for Hard grade', () => { - const d = initialDifficulty(Grade.Hard); - // w4 - e^(w5*(2-1)) + 1 = 7.1949 - e^0.5345 + 1 - expect(d).toBeGreaterThan(5); - expect(d).toBeLessThan(7.19); - }); - - it('should return moderate difficulty for Good grade', () => { - const d = initialDifficulty(Grade.Good); - // w4 - e^(w5*(3-1)) + 1 - expect(d).toBeGreaterThan(4); - expect(d).toBeLessThan(6); - }); - - it('should return lowest difficulty for Easy grade', () => { - const d = initialDifficulty(Grade.Easy); - expect(d).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_DIFFICULTY); - expect(d).toBeLessThan(5); - }); - - it('should produce decreasing difficulty as grade increases', () => { - const dAgain = initialDifficulty(Grade.Again); - const dHard = initialDifficulty(Grade.Hard); - const dGood = initialDifficulty(Grade.Good); - const dEasy = initialDifficulty(Grade.Easy); - - expect(dAgain).toBeGreaterThan(dHard); - expect(dHard).toBeGreaterThan(dGood); - expect(dGood).toBeGreaterThan(dEasy); - }); - - it('should always clamp difficulty to minimum 1', () => { - // Even with extreme custom weights, difficulty should be >= 1 - const customWeights = [0, 0, 0, 0, -100, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - const d = initialDifficulty(Grade.Easy, customWeights); - expect(d).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_DIFFICULTY); - }); - - it('should always clamp difficulty to maximum 10', () => { - // Even with extreme custom weights, difficulty should be <= 10 - const customWeights = [0, 0, 0, 0, 100, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - const d = initialDifficulty(Grade.Again, customWeights); - expect(d).toBeLessThanOrEqual(FSRS_CONSTANTS.MAX_DIFFICULTY); - }); - }); - - // ========================================================================== - // 2. INITIAL STABILITY TESTS - // ========================================================================== - - describe('initialStability', () => { - it('should return lowest stability for Again grade', () => { - const s = initialStability(Grade.Again); - // w[0] = 0.40255 - expect(s).toBeCloseTo(0.40255, 3); - }); - - it('should return higher stability for Hard grade', () => { - const s = initialStability(Grade.Hard); - // w[1] = 1.18385 - expect(s).toBeCloseTo(1.18385, 3); - }); - - it('should return higher stability for Good grade', () => { - const s = initialStability(Grade.Good); - // w[2] = 3.173 - expect(s).toBeCloseTo(3.173, 3); - }); - - it('should return highest stability for Easy grade', () => { - const s = initialStability(Grade.Easy); - // w[3] = 15.69105 - expect(s).toBeCloseTo(15.69105, 3); - }); - - it('should produce increasing stability as grade increases', () => { - const sAgain = initialStability(Grade.Again); - const sHard = initialStability(Grade.Hard); - const sGood = initialStability(Grade.Good); - const sEasy = initialStability(Grade.Easy); - - expect(sAgain).toBeLessThan(sHard); - expect(sHard).toBeLessThan(sGood); - expect(sGood).toBeLessThan(sEasy); - }); - - it('should always return positive stability', () => { - for (const grade of [Grade.Again, Grade.Hard, Grade.Good, Grade.Easy]) { - const s = initialStability(grade); - expect(s).toBeGreaterThan(0); - expect(s).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_STABILITY); - } - }); - - it('should use minimum stability when custom weight is zero', () => { - const customWeights = [0, 0, 0, 0, 7, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - const s = initialStability(Grade.Again, customWeights); - expect(s).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_STABILITY); - }); - }); - - // ========================================================================== - // 3. RETRIEVABILITY TESTS - // ========================================================================== - - describe('retrievability', () => { - it('should return 1.0 when elapsed days is 0', () => { - const r = retrievability(10, 0); - expect(r).toBe(1); - }); - - it('should return 1.0 when elapsed days is negative', () => { - const r = retrievability(10, -5); - expect(r).toBe(1); - }); - - it('should return 0 when stability is 0', () => { - const r = retrievability(0, 10); - expect(r).toBe(0); - }); - - it('should return 0 when stability is negative', () => { - const r = retrievability(-5, 10); - expect(r).toBe(0); - }); - - it('should decay exponentially over time', () => { - const stability = 10; - const r1 = retrievability(stability, 1); - const r5 = retrievability(stability, 5); - const r10 = retrievability(stability, 10); - const r30 = retrievability(stability, 30); - - // Each subsequent measurement should be lower - expect(r1).toBeGreaterThan(r5); - expect(r5).toBeGreaterThan(r10); - expect(r10).toBeGreaterThan(r30); - - // All should be in valid range - expect(r1).toBeLessThanOrEqual(1); - expect(r30).toBeGreaterThan(0); - }); - - it('should never return negative values', () => { - const r = retrievability(1, 1000); // Very large elapsed time - expect(r).toBeGreaterThanOrEqual(0); - }); - - it('should never exceed 1', () => { - const r = retrievability(1000, 0.001); // Very small elapsed time - expect(r).toBeLessThanOrEqual(1); - }); - - it('should decay slower with higher stability', () => { - const elapsedDays = 10; - const rLowStability = retrievability(5, elapsedDays); - const rHighStability = retrievability(50, elapsedDays); - - expect(rHighStability).toBeGreaterThan(rLowStability); - }); - - it('should follow FSRS-5 power forgetting curve formula', () => { - // R = (1 + t/(9*S))^(-1) - const stability = 10; - const elapsed = 9; // After 9 days with S=10 - const expected = Math.pow(1 + elapsed / (9 * stability), -1); - const actual = retrievability(stability, elapsed); - expect(actual).toBeCloseTo(expected, 6); - }); - }); - - // ========================================================================== - // 4. NEXT DIFFICULTY TESTS - // ========================================================================== - - describe('nextDifficulty', () => { - it('should increase difficulty on Again grade', () => { - const currentD = 5; - const newD = nextDifficulty(currentD, Grade.Again); - expect(newD).toBeGreaterThan(currentD); - }); - - it('should slightly increase difficulty on Hard grade', () => { - const currentD = 5; - const newD = nextDifficulty(currentD, Grade.Hard); - expect(newD).toBeGreaterThan(currentD); - }); - - it('should maintain difficulty on Good grade (near target)', () => { - const currentD = 5; - const newD = nextDifficulty(currentD, Grade.Good); - // Good grade (3) is the reference point, so difficulty should stay similar - // with mean reversion pulling towards initial Good difficulty - expect(Math.abs(newD - currentD)).toBeLessThan(1); - }); - - it('should decrease difficulty on Easy grade', () => { - const currentD = 5; - const newD = nextDifficulty(currentD, Grade.Easy); - expect(newD).toBeLessThan(currentD); - }); - - it('should apply mean reversion towards initial difficulty', () => { - // Very high difficulty should regress towards mean - const highD = 9; - const newDHigh = nextDifficulty(highD, Grade.Good); - expect(newDHigh).toBeLessThan(highD); - - // Very low difficulty should regress towards mean - const lowD = 2; - const newDLow = nextDifficulty(lowD, Grade.Good); - expect(newDLow).toBeGreaterThan(lowD); - }); - - it('should clamp to minimum difficulty 1', () => { - const newD = nextDifficulty(1, Grade.Easy); - expect(newD).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_DIFFICULTY); - }); - - it('should clamp to maximum difficulty 10', () => { - const newD = nextDifficulty(10, Grade.Again); - expect(newD).toBeLessThanOrEqual(FSRS_CONSTANTS.MAX_DIFFICULTY); - }); - }); - - // ========================================================================== - // 5. NEXT RECALL STABILITY TESTS - // ========================================================================== - - describe('nextRecallStability', () => { - it('should increase stability on Good grade', () => { - const currentS = 10; - const difficulty = 5; - const retrievabilityR = 0.9; - const newS = nextRecallStability(currentS, difficulty, retrievabilityR, Grade.Good); - expect(newS).toBeGreaterThan(currentS); - }); - - it('should increase stability on Easy grade more than Good', () => { - const currentS = 10; - const difficulty = 5; - const retrievabilityR = 0.9; - const newSGood = nextRecallStability(currentS, difficulty, retrievabilityR, Grade.Good); - const newSEasy = nextRecallStability(currentS, difficulty, retrievabilityR, Grade.Easy); - expect(newSEasy).toBeGreaterThan(newSGood); - }); - - it('should increase stability on Hard grade less than Good', () => { - const currentS = 10; - const difficulty = 5; - const retrievabilityR = 0.9; - const newSHard = nextRecallStability(currentS, difficulty, retrievabilityR, Grade.Hard); - const newSGood = nextRecallStability(currentS, difficulty, retrievabilityR, Grade.Good); - expect(newSGood).toBeGreaterThan(newSHard); - }); - - it('should delegate to nextForgetStability for Again grade', () => { - const currentS = 10; - const difficulty = 5; - const retrievabilityR = 0.9; - const newS = nextRecallStability(currentS, difficulty, retrievabilityR, Grade.Again); - // Again grade should result in reduced stability (lapse) - expect(newS).toBeLessThan(currentS); - }); - - it('should produce higher stability growth with lower difficulty', () => { - const currentS = 10; - const retrievabilityR = 0.9; - const newSLowD = nextRecallStability(currentS, 2, retrievabilityR, Grade.Good); - const newSHighD = nextRecallStability(currentS, 8, retrievabilityR, Grade.Good); - expect(newSLowD).toBeGreaterThan(newSHighD); - }); - - it('should produce higher stability growth with lower retrievability', () => { - // Lower R means more "desirable difficulty" - forgetting curve benefit - const currentS = 10; - const difficulty = 5; - const newSHighR = nextRecallStability(currentS, difficulty, 0.95, Grade.Good); - const newSLowR = nextRecallStability(currentS, difficulty, 0.7, Grade.Good); - expect(newSLowR).toBeGreaterThan(newSHighR); - }); - - it('should clamp to maximum stability', () => { - const currentS = 30000; - const newS = nextRecallStability(currentS, 1, 0.5, Grade.Easy); - expect(newS).toBeLessThanOrEqual(FSRS_CONSTANTS.MAX_STABILITY); - }); - }); - - // ========================================================================== - // 6. NEXT FORGET STABILITY TESTS - // ========================================================================== - - describe('nextForgetStability', () => { - it('should return stability lower than current after lapse', () => { - const currentS = 50; - const difficulty = 5; - const newS = nextForgetStability(difficulty, currentS); - expect(newS).toBeLessThan(currentS); - }); - - it('should produce lower stability with higher difficulty', () => { - const currentS = 50; - const newSLowD = nextForgetStability(2, currentS); - const newSHighD = nextForgetStability(9, currentS); - expect(newSLowD).toBeGreaterThan(newSHighD); - }); - - it('should preserve some memory (not reset to minimum)', () => { - const currentS = 100; - const newS = nextForgetStability(5, currentS); - // After lapse, some memory trace remains - expect(newS).toBeGreaterThan(FSRS_CONSTANTS.MIN_STABILITY); - }); - - it('should never return negative stability', () => { - const newS = nextForgetStability(10, 1); - expect(newS).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_STABILITY); - }); - - it('should account for retrievability at time of lapse', () => { - const currentS = 50; - const difficulty = 5; - const newSHighR = nextForgetStability(difficulty, currentS, 0.9); - const newSLowR = nextForgetStability(difficulty, currentS, 0.3); - // FSRS-5 formula: S'(f) = w11 * D^(-w12) * ((S+1)^w13 - 1) * e^(w14*(1-R)) - // Lower R means e^(w14*(1-R)) is larger, so new stability is actually higher - // This reflects that forgetting when memory was already weak - // preserves more of the memory trace than forgetting at high retrievability - expect(newSLowR).toBeGreaterThan(newSHighR); - }); - }); - - // ========================================================================== - // 7. NEXT INTERVAL TESTS - // ========================================================================== - - describe('nextInterval', () => { - it('should return 0 for zero stability', () => { - const interval = nextInterval(0); - expect(interval).toBe(0); - }); - - it('should return 0 for negative stability', () => { - const interval = nextInterval(-10); - expect(interval).toBe(0); - }); - - it('should return 0 for 100% desired retention', () => { - const interval = nextInterval(10, 1); - expect(interval).toBe(0); - }); - - it('should return maximum for 0% desired retention', () => { - const interval = nextInterval(10, 0); - expect(interval).toBe(FSRS_CONSTANTS.MAX_STABILITY); - }); - - it('should return longer intervals for higher stability', () => { - const intervalLow = nextInterval(5); - const intervalHigh = nextInterval(50); - expect(intervalHigh).toBeGreaterThan(intervalLow); - }); - - it('should return shorter intervals for higher desired retention', () => { - const stability = 20; - const intervalLowRetention = nextInterval(stability, 0.8); - const intervalHighRetention = nextInterval(stability, 0.95); - expect(intervalLowRetention).toBeGreaterThan(intervalHighRetention); - }); - - it('should follow FSRS-5 interval formula', () => { - // I = 9 * S * (R^(-1) - 1) - const stability = 10; - const retention = 0.9; - const expected = Math.round(9 * stability * (Math.pow(retention, -1) - 1)); - const actual = nextInterval(stability, retention); - expect(actual).toBe(expected); - }); - - it('should return minimum 1 day for non-zero stability with default retention', () => { - // Very low stability should still produce at least some interval - const interval = nextInterval(FSRS_CONSTANTS.MIN_STABILITY); - expect(interval).toBeGreaterThanOrEqual(0); - }); - - it('should round interval to nearest integer', () => { - const interval = nextInterval(7.5, 0.85); - expect(Number.isInteger(interval)).toBe(true); - }); - }); - - // ========================================================================== - // 8. FULL REVIEW FLOW TESTS - // ========================================================================== - - describe('full review flow', () => { - it('should initialize a new card correctly', () => { - const card = scheduler.newCard(); - expect(card.state).toBe('New'); - expect(card.reps).toBe(0); - expect(card.lapses).toBe(0); - expect(card.stability).toBeGreaterThan(0); - expect(card.difficulty).toBeGreaterThan(0); - }); - - it('should progress new item through first review', () => { - const card = scheduler.newCard(); - const result = scheduler.review(card, Grade.Good, 0); - - expect(result.state.reps).toBe(1); - expect(result.state.stability).toBeGreaterThan(0); - expect(result.state.state).toBe('Review'); - expect(result.retrievability).toBe(1); // First review = 100% retrievability - }); - - it('should handle first review with Again grade', () => { - const card = scheduler.newCard(); - const result = scheduler.review(card, Grade.Again, 0); - - expect(result.state.reps).toBe(1); - expect(result.state.lapses).toBe(1); - expect(result.state.state).toBe('Learning'); - }); - - it('should handle first review with Hard grade', () => { - const card = scheduler.newCard(); - const result = scheduler.review(card, Grade.Hard, 0); - - expect(result.state.reps).toBe(1); - expect(result.state.state).toBe('Learning'); - }); - - it('should progress through multiple reviews', () => { - let card = scheduler.newCard(); - - // First review - Good - let result = scheduler.review(card, Grade.Good, 0); - card = result.state; - expect(card.reps).toBe(1); - - // Wait scheduled interval, second review - Good - result = scheduler.review(card, Grade.Good, result.interval); - card = result.state; - expect(card.reps).toBe(2); - expect(card.state).toBe('Review'); - - // Third review - Easy - result = scheduler.review(card, Grade.Easy, result.interval); - card = result.state; - expect(card.reps).toBe(3); - }); - - it('should handle lapse correctly', () => { - // Setup: card with established stability - const state: FSRSState = { - stability: 100, - difficulty: 5, - state: 'Review', - reps: 10, - lapses: 0, - lastReview: new Date(), - scheduledDays: 100, - }; - - const result = scheduler.review(state, Grade.Again, 100); - - expect(result.state.stability).toBeLessThan(state.stability); - expect(result.state.lapses).toBe(1); - expect(result.state.state).toBe('Relearning'); - expect(result.isLapse).toBe(true); - }); - - it('should recover from lapse with subsequent Good reviews', () => { - // Start with a lapse state - let state: FSRSState = { - stability: 10, // Post-lapse stability - difficulty: 6, - state: 'Relearning', - reps: 5, - lapses: 1, - lastReview: new Date(), - scheduledDays: 1, - }; - - // Good review after lapse - let result = scheduler.review(state, Grade.Good, 1); - state = result.state; - expect(state.state).toBe('Review'); - - // Another Good review - result = scheduler.review(state, Grade.Good, result.interval); - state = result.state; - expect(state.stability).toBeGreaterThan(10); - }); - - it('should not mark first Again as lapse', () => { - const card = scheduler.newCard(); - const result = scheduler.review(card, Grade.Again, 0); - - // First review Again counts as a lapse in the code (lapses = 1) - // but isLapse flag should be false since it's from New state - expect(result.isLapse).toBe(false); - }); - - it('should increase stability faster with Easy grade', () => { - const card = scheduler.newCard(); - - // Two parallel paths: one with Good, one with Easy - const resultGood = scheduler.review(card, Grade.Good, 0); - const resultEasy = scheduler.review(card, Grade.Easy, 0); - - expect(resultEasy.state.stability).toBeGreaterThan(resultGood.state.stability); - expect(resultEasy.interval).toBeGreaterThan(resultGood.interval); - }); - }); - - // ========================================================================== - // 9. SENTIMENT BOOST TESTS - // ========================================================================== - - describe('applySentimentBoost', () => { - it('should apply no boost when sentiment intensity is 0', () => { - const stability = 10; - const boosted = applySentimentBoost(stability, 0); - expect(boosted).toBe(stability); - }); - - it('should apply maximum boost when sentiment intensity is 1', () => { - const stability = 10; - const maxBoost = 2.0; - const boosted = applySentimentBoost(stability, 1, maxBoost); - expect(boosted).toBe(stability * maxBoost); - }); - - it('should apply proportional boost for intermediate sentiment', () => { - const stability = 10; - const boosted = applySentimentBoost(stability, 0.5, 2.0); - // boost = 1 + (2 - 1) * 0.5 = 1.5 - expect(boosted).toBe(stability * 1.5); - }); - - it('should clamp sentiment intensity to [0, 1]', () => { - const stability = 10; - const boostedNegative = applySentimentBoost(stability, -0.5, 2.0); - const boostedOverflow = applySentimentBoost(stability, 1.5, 2.0); - - expect(boostedNegative).toBe(stability); // 0 sentiment = no boost - expect(boostedOverflow).toBe(stability * 2.0); // 1.0 clamped - }); - - it('should clamp max boost to [1, 3]', () => { - const stability = 10; - const boostedLowMax = applySentimentBoost(stability, 1, 0.5); - const boostedHighMax = applySentimentBoost(stability, 1, 5); - - expect(boostedLowMax).toBe(stability); // min boost = 1 - expect(boostedHighMax).toBe(stability * 3); // max boost clamped to 3 - }); - - it('should integrate with scheduler when enabled', () => { - const schedulerWithBoost = new FSRSScheduler({ - enableSentimentBoost: true, - maxSentimentBoost: 2, - }); - - const card = schedulerWithBoost.newCard(); - const resultNoBoost = schedulerWithBoost.review(card, Grade.Good, 0); - const resultWithBoost = schedulerWithBoost.review(card, Grade.Good, 0, 0.5); - - expect(resultWithBoost.state.stability).toBeGreaterThan(resultNoBoost.state.stability); - }); - - it('should not apply boost when sentiment is undefined', () => { - const card = scheduler.newCard(); - const result = scheduler.review(card, Grade.Good, 0, undefined); - const resultExplicitZero = scheduler.review(card, Grade.Good, 0, 0); - - expect(result.state.stability).toBe(resultExplicitZero.state.stability); - }); - - it('should not apply boost when disabled in config', () => { - const schedulerNoBoost = new FSRSScheduler({ - enableSentimentBoost: false, - }); - - const card = schedulerNoBoost.newCard(); - const result = schedulerNoBoost.review(card, Grade.Good, 0, 1.0); - const resultNoSentiment = schedulerNoBoost.review(card, Grade.Good, 0); - - expect(result.state.stability).toBe(resultNoSentiment.state.stability); - }); - }); - - // ========================================================================== - // 10. EDGE CASES - // ========================================================================== - - describe('edge cases', () => { - it('should handle very large elapsed days', () => { - const state: FSRSState = { - stability: 10, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: new Date(), - scheduledDays: 10, - }; - - // 10 years later - const result = scheduler.review(state, Grade.Good, 3650); - - expect(result.retrievability).toBeGreaterThanOrEqual(0); - expect(result.retrievability).toBeLessThan(0.1); // Should be very low - expect(result.state.stability).toBeGreaterThan(0); - }); - - it('should handle zero elapsed days correctly', () => { - const state: FSRSState = { - stability: 10, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: new Date(), - scheduledDays: 10, - }; - - const result = scheduler.review(state, Grade.Good, 0); - - expect(result.retrievability).toBe(1); - }); - - it('should handle boundary grade values', () => { - const card = scheduler.newCard(); - - // Minimum grade (1) - const resultAgain = scheduler.review(card, 1 as ReviewGrade, 0); - expect(resultAgain.state.reps).toBe(1); - - // Maximum grade (4) - const resultEasy = scheduler.review(card, 4 as ReviewGrade, 0); - expect(resultEasy.state.reps).toBe(1); - }); - - it('should handle minimum stability edge case', () => { - const state: FSRSState = { - stability: FSRS_CONSTANTS.MIN_STABILITY, - difficulty: 10, - state: 'Relearning', - reps: 1, - lapses: 1, - lastReview: new Date(), - scheduledDays: 0, - }; - - const result = scheduler.review(state, Grade.Again, 1); - - expect(result.state.stability).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_STABILITY); - }); - - it('should handle maximum difficulty edge case', () => { - const state: FSRSState = { - stability: 10, - difficulty: FSRS_CONSTANTS.MAX_DIFFICULTY, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: new Date(), - scheduledDays: 10, - }; - - const result = scheduler.review(state, Grade.Again, 10); - - expect(result.state.difficulty).toBeLessThanOrEqual(FSRS_CONSTANTS.MAX_DIFFICULTY); - }); - - it('should handle rapid consecutive reviews', () => { - let card = scheduler.newCard(); - - // Review 5 times in quick succession - for (let i = 0; i < 5; i++) { - const result = scheduler.review(card, Grade.Good, 0); - card = result.state; - } - - expect(card.reps).toBe(5); - expect(card.stability).toBeGreaterThan(0); - }); - - it('should handle alternating grades', () => { - let card = scheduler.newCard(); - const grades = [Grade.Good, Grade.Again, Grade.Easy, Grade.Hard, Grade.Good]; - - for (const grade of grades) { - const result = scheduler.review(card, grade, 0); - card = result.state; - } - - expect(card.reps).toBe(5); - expect(card.lapses).toBeGreaterThanOrEqual(1); - }); - }); - - // ========================================================================== - // SERIALIZATION TESTS - // ========================================================================== - - describe('serialization', () => { - it('should serialize FSRSState to JSON', () => { - const state: FSRSState = { - stability: 10, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: new Date('2024-01-15T10:30:00.000Z'), - scheduledDays: 10, - }; - - const json = serializeFSRSState(state); - expect(typeof json).toBe('string'); - expect(json).toContain('"stability":10'); - expect(json).toContain('"2024-01-15T10:30:00.000Z"'); - }); - - it('should deserialize FSRSState from JSON', () => { - const original: FSRSState = { - stability: 10, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: new Date('2024-01-15T10:30:00.000Z'), - scheduledDays: 10, - }; - - const json = serializeFSRSState(original); - const deserialized = deserializeFSRSState(json); - - expect(deserialized.stability).toBe(original.stability); - expect(deserialized.difficulty).toBe(original.difficulty); - expect(deserialized.state).toBe(original.state); - expect(deserialized.reps).toBe(original.reps); - expect(deserialized.lapses).toBe(original.lapses); - expect(deserialized.lastReview.toISOString()).toBe(original.lastReview.toISOString()); - }); - - it('should round-trip FSRSState correctly', () => { - const card = scheduler.newCard(); - const result = scheduler.review(card, Grade.Good, 0); - - const json = serializeFSRSState(result.state); - const restored = deserializeFSRSState(json); - - expect(restored.stability).toBeCloseTo(result.state.stability, 5); - expect(restored.difficulty).toBeCloseTo(result.state.difficulty, 5); - expect(restored.state).toBe(result.state.state); - }); - }); - - // ========================================================================== - // UTILITY FUNCTION TESTS - // ========================================================================== - - describe('utility functions', () => { - it('optimalReviewTime should match nextInterval', () => { - const state: FSRSState = { - stability: 20, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: new Date(), - scheduledDays: 20, - }; - - const optimal = optimalReviewTime(state); - const interval = nextInterval(state.stability); - - expect(optimal).toBe(interval); - }); - - it('optimalReviewTime should respect custom retention', () => { - const state: FSRSState = { - stability: 20, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: new Date(), - scheduledDays: 20, - }; - - const optimalDefault = optimalReviewTime(state); - const optimalHighRetention = optimalReviewTime(state, 0.95); - - expect(optimalHighRetention).toBeLessThan(optimalDefault); - }); - - it('isReviewDue should return true when scheduled days passed', () => { - const pastDate = new Date(); - pastDate.setDate(pastDate.getDate() - 15); - - const state: FSRSState = { - stability: 20, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: pastDate, - scheduledDays: 10, // Due after 10 days, 15 have passed - }; - - expect(isReviewDue(state)).toBe(true); - }); - - it('isReviewDue should return false when not yet due', () => { - const recentDate = new Date(); - recentDate.setDate(recentDate.getDate() - 2); - - const state: FSRSState = { - stability: 20, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: recentDate, - scheduledDays: 10, // Due after 10 days, only 2 have passed - }; - - expect(isReviewDue(state)).toBe(false); - }); - - it('isReviewDue should use retention threshold when provided', () => { - const pastDate = new Date(); - pastDate.setDate(pastDate.getDate() - 5); - - const state: FSRSState = { - stability: 10, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: pastDate, - scheduledDays: 10, - }; - - // With high retention threshold, should be due sooner - const dueHighRetention = isReviewDue(state, 0.95); - // With low retention threshold, should not be due yet - const dueLowRetention = isReviewDue(state, 0.5); - - // Retrievability after 5 days with stability 10: - // R = (1 + 5/(9*10))^(-1) = 0.947... - expect(dueHighRetention).toBe(true); // R < 0.95 - expect(dueLowRetention).toBe(false); // R > 0.5 - }); - }); - - // ========================================================================== - // SCHEDULER CONFIGURATION TESTS - // ========================================================================== - - describe('scheduler configuration', () => { - it('should use default configuration when none provided', () => { - const config = scheduler.getConfig(); - - expect(config.desiredRetention).toBe(0.9); - expect(config.maximumInterval).toBe(36500); - expect(config.enableSentimentBoost).toBe(true); - expect(config.maxSentimentBoost).toBe(2); - }); - - it('should accept custom desired retention', () => { - const customScheduler = new FSRSScheduler({ desiredRetention: 0.85 }); - const config = customScheduler.getConfig(); - - expect(config.desiredRetention).toBe(0.85); - }); - - it('should accept custom maximum interval', () => { - const customScheduler = new FSRSScheduler({ maximumInterval: 365 }); - const config = customScheduler.getConfig(); - - expect(config.maximumInterval).toBe(365); - }); - - it('should clamp interval to maximum', () => { - const customScheduler = new FSRSScheduler({ maximumInterval: 30 }); - - const state: FSRSState = { - stability: 100, // Would normally give interval > 30 - difficulty: 5, - state: 'Review', - reps: 10, - lapses: 0, - lastReview: new Date(), - scheduledDays: 100, - }; - - const result = customScheduler.review(state, Grade.Easy, 100); - expect(result.interval).toBeLessThanOrEqual(30); - }); - - it('should use custom weights when provided', () => { - const customWeights = Array(19).fill(1); - const customScheduler = new FSRSScheduler({ weights: customWeights }); - const weights = customScheduler.getWeights(); - - expect(weights.length).toBe(19); - expect(weights[0]).toBe(1); - }); - - it('should use default weights when none provided', () => { - const weights = scheduler.getWeights(); - - expect(weights.length).toBe(19); - expect(weights[0]).toBeCloseTo(FSRS_WEIGHTS[0], 5); - }); - - it('should preview all review outcomes', () => { - const card = scheduler.newCard(); - const previews = scheduler.previewReviews(card, 0); - - expect(previews.again).toBeDefined(); - expect(previews.hard).toBeDefined(); - expect(previews.good).toBeDefined(); - expect(previews.easy).toBeDefined(); - - expect(previews.again.state.lapses).toBeGreaterThanOrEqual(1); - expect(previews.easy.interval).toBeGreaterThan(previews.good.interval); - }); - - it('should get retrievability for a state', () => { - const state: FSRSState = { - stability: 10, - difficulty: 5, - state: 'Review', - reps: 5, - lapses: 0, - lastReview: new Date(), - scheduledDays: 10, - }; - - const r = scheduler.getRetrievability(state, 5); - expect(r).toBeGreaterThan(0); - expect(r).toBeLessThan(1); - }); - }); - - // ========================================================================== - // FSRS CONSTANTS TESTS - // ========================================================================== - - describe('FSRS constants', () => { - it('should have correct weight count', () => { - expect(FSRS_WEIGHTS.length).toBe(19); - }); - - it('should have valid difficulty bounds', () => { - expect(FSRS_CONSTANTS.MIN_DIFFICULTY).toBe(1); - expect(FSRS_CONSTANTS.MAX_DIFFICULTY).toBe(10); - }); - - it('should have valid stability bounds', () => { - expect(FSRS_CONSTANTS.MIN_STABILITY).toBeGreaterThan(0); - expect(FSRS_CONSTANTS.MAX_STABILITY).toBe(36500); - }); - - it('should have reasonable default retention', () => { - expect(FSRS_CONSTANTS.DEFAULT_RETENTION).toBe(0.9); - }); - }); -}); diff --git a/packages/core/src/__tests__/database.test.ts b/packages/core/src/__tests__/database.test.ts deleted file mode 100644 index 302269f..0000000 --- a/packages/core/src/__tests__/database.test.ts +++ /dev/null @@ -1,476 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from '@rstest/core'; -import Database from 'better-sqlite3'; -import { nanoid } from 'nanoid'; -import { - createTestDatabase, - createTestNode, - createTestPerson, - createTestEdge, - cleanupTestDatabase, - generateTestId, -} from './setup.js'; - -describe('VestigeDatabase', () => { - let db: Database.Database; - - beforeEach(() => { - db = createTestDatabase(); - }); - - afterEach(() => { - cleanupTestDatabase(db); - }); - - describe('Schema Setup', () => { - it('should create all required tables', () => { - const tables = db.prepare( - "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" - ).all() as { name: string }[]; - - const tableNames = tables.map(t => t.name); - - expect(tableNames).toContain('knowledge_nodes'); - expect(tableNames).toContain('knowledge_fts'); - expect(tableNames).toContain('people'); - expect(tableNames).toContain('interactions'); - expect(tableNames).toContain('graph_edges'); - expect(tableNames).toContain('sources'); - expect(tableNames).toContain('embeddings'); - expect(tableNames).toContain('vestige_metadata'); - }); - - it('should create required indexes', () => { - const indexes = db.prepare( - "SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'" - ).all() as { name: string }[]; - - const indexNames = indexes.map(i => i.name); - - expect(indexNames).toContain('idx_nodes_created_at'); - expect(indexNames).toContain('idx_nodes_last_accessed'); - expect(indexNames).toContain('idx_nodes_retention'); - expect(indexNames).toContain('idx_people_name'); - expect(indexNames).toContain('idx_edges_from'); - expect(indexNames).toContain('idx_edges_to'); - }); - }); - - describe('insertNode', () => { - it('should create a new knowledge node', () => { - const id = nanoid(); - const now = new Date().toISOString(); - const nodeData = createTestNode({ - content: 'Test knowledge content', - tags: ['test', 'knowledge'], - }); - - const stmt = db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, summary, - created_at, updated_at, last_accessed_at, access_count, - retention_strength, stability_factor, sentiment_intensity, - source_type, source_platform, - confidence, people, concepts, events, tags - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, - nodeData.content, - null, - now, - now, - now, - 0, - 1.0, - 1.0, - 0, - nodeData.sourceType, - nodeData.sourcePlatform, - 0.8, - JSON.stringify(nodeData.people), - JSON.stringify(nodeData.concepts), - JSON.stringify(nodeData.events), - JSON.stringify(nodeData.tags) - ); - - const result = db.prepare('SELECT * FROM knowledge_nodes WHERE id = ?').get(id) as Record; - - expect(result).toBeDefined(); - expect(result['content']).toBe('Test knowledge content'); - expect(JSON.parse(result['tags'] as string)).toContain('test'); - expect(JSON.parse(result['tags'] as string)).toContain('knowledge'); - }); - - it('should store retention and stability factors', () => { - const id = nanoid(); - const now = new Date().toISOString(); - const nodeData = createTestNode(); - - const stmt = db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, - created_at, updated_at, last_accessed_at, - retention_strength, stability_factor, sentiment_intensity, - storage_strength, retrieval_strength, - source_type, source_platform, - confidence, people, concepts, events, tags - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, - nodeData.content, - now, - now, - now, - 0.85, - 2.5, - 0.7, - 1.5, - 0.9, - nodeData.sourceType, - nodeData.sourcePlatform, - 0.8, - '[]', - '[]', - '[]', - '[]' - ); - - const result = db.prepare('SELECT * FROM knowledge_nodes WHERE id = ?').get(id) as Record; - - expect(result['retention_strength']).toBe(0.85); - expect(result['stability_factor']).toBe(2.5); - expect(result['sentiment_intensity']).toBe(0.7); - expect(result['storage_strength']).toBe(1.5); - expect(result['retrieval_strength']).toBe(0.9); - }); - }); - - describe('searchNodes', () => { - beforeEach(() => { - // Insert test nodes for searching - const nodes = [ - { id: generateTestId(), content: 'TypeScript is a typed superset of JavaScript' }, - { id: generateTestId(), content: 'React is a JavaScript library for building user interfaces' }, - { id: generateTestId(), content: 'Python is a versatile programming language' }, - ]; - - const stmt = db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, created_at, updated_at, last_accessed_at, - source_type, source_platform, confidence, people, concepts, events, tags - ) VALUES (?, ?, datetime('now'), datetime('now'), datetime('now'), 'manual', 'manual', 0.8, '[]', '[]', '[]', '[]') - `); - - for (const node of nodes) { - stmt.run(node.id, node.content); - } - }); - - it('should find nodes by keyword using FTS', () => { - const results = db.prepare(` - SELECT kn.* FROM knowledge_nodes kn - JOIN knowledge_fts fts ON kn.id = fts.id - WHERE knowledge_fts MATCH ? - ORDER BY rank - `).all('JavaScript') as Record[]; - - expect(results.length).toBe(2); - expect(results.some(r => (r['content'] as string).includes('TypeScript'))).toBe(true); - expect(results.some(r => (r['content'] as string).includes('React'))).toBe(true); - }); - - it('should not find unrelated content', () => { - const results = db.prepare(` - SELECT kn.* FROM knowledge_nodes kn - JOIN knowledge_fts fts ON kn.id = fts.id - WHERE knowledge_fts MATCH ? - `).all('Rust') as Record[]; - - expect(results.length).toBe(0); - }); - - it('should find partial matches', () => { - const results = db.prepare(` - SELECT kn.* FROM knowledge_nodes kn - JOIN knowledge_fts fts ON kn.id = fts.id - WHERE knowledge_fts MATCH ? - `).all('programming') as Record[]; - - expect(results.length).toBe(1); - expect((results[0]['content'] as string)).toContain('Python'); - }); - }); - - describe('People Operations', () => { - it('should insert a person', () => { - const id = nanoid(); - const now = new Date().toISOString(); - const personData = createTestPerson({ - name: 'John Doe', - relationshipType: 'friend', - organization: 'Acme Inc', - }); - - const stmt = db.prepare(` - INSERT INTO people ( - id, name, aliases, relationship_type, organization, - contact_frequency, shared_topics, shared_projects, relationship_health, - social_links, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, - personData.name, - JSON.stringify(personData.aliases), - personData.relationshipType, - personData.organization, - personData.contactFrequency, - JSON.stringify(personData.sharedTopics), - JSON.stringify(personData.sharedProjects), - personData.relationshipHealth, - JSON.stringify(personData.socialLinks), - now, - now - ); - - const result = db.prepare('SELECT * FROM people WHERE id = ?').get(id) as Record; - - expect(result).toBeDefined(); - expect(result['name']).toBe('John Doe'); - expect(result['relationship_type']).toBe('friend'); - expect(result['organization']).toBe('Acme Inc'); - }); - - it('should find person by name', () => { - const id = nanoid(); - const now = new Date().toISOString(); - - db.prepare(` - INSERT INTO people (id, name, aliases, social_links, shared_topics, shared_projects, created_at, updated_at) - VALUES (?, ?, '[]', '{}', '[]', '[]', ?, ?) - `).run(id, 'Jane Smith', now, now); - - const result = db.prepare('SELECT * FROM people WHERE name = ?').get('Jane Smith') as Record; - - expect(result).toBeDefined(); - expect(result['id']).toBe(id); - }); - - it('should find person by alias', () => { - const id = nanoid(); - const now = new Date().toISOString(); - - db.prepare(` - INSERT INTO people (id, name, aliases, social_links, shared_topics, shared_projects, created_at, updated_at) - VALUES (?, ?, ?, '{}', '[]', '[]', ?, ?) - `).run(id, 'Robert Johnson', JSON.stringify(['Bob', 'Bobby']), now, now); - - const result = db.prepare(` - SELECT * FROM people WHERE name = ? OR aliases LIKE ? - `).get('Bob', '%"Bob"%') as Record; - - expect(result).toBeDefined(); - expect(result['name']).toBe('Robert Johnson'); - }); - }); - - describe('Graph Edges', () => { - let nodeId1: string; - let nodeId2: string; - - beforeEach(() => { - nodeId1 = nanoid(); - nodeId2 = nanoid(); - const now = new Date().toISOString(); - - // Create two nodes - const stmt = db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, created_at, updated_at, last_accessed_at, - source_type, source_platform, confidence, people, concepts, events, tags - ) VALUES (?, ?, ?, ?, ?, 'manual', 'manual', 0.8, '[]', '[]', '[]', '[]') - `); - - stmt.run(nodeId1, 'Node 1 content', now, now, now); - stmt.run(nodeId2, 'Node 2 content', now, now, now); - }); - - it('should create an edge between nodes', () => { - const edgeId = nanoid(); - const now = new Date().toISOString(); - const edgeData = createTestEdge(nodeId1, nodeId2, { - edgeType: 'relates_to', - weight: 0.8, - }); - - db.prepare(` - INSERT INTO graph_edges (id, from_id, to_id, edge_type, weight, metadata, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).run(edgeId, edgeData.fromId, edgeData.toId, edgeData.edgeType, edgeData.weight, '{}', now); - - const result = db.prepare('SELECT * FROM graph_edges WHERE id = ?').get(edgeId) as Record; - - expect(result).toBeDefined(); - expect(result['from_id']).toBe(nodeId1); - expect(result['to_id']).toBe(nodeId2); - expect(result['edge_type']).toBe('relates_to'); - expect(result['weight']).toBe(0.8); - }); - - it('should find related nodes', () => { - const edgeId = nanoid(); - const now = new Date().toISOString(); - - db.prepare(` - INSERT INTO graph_edges (id, from_id, to_id, edge_type, weight, metadata, created_at) - VALUES (?, ?, ?, 'relates_to', 0.5, '{}', ?) - `).run(edgeId, nodeId1, nodeId2, now); - - const results = db.prepare(` - SELECT DISTINCT - CASE WHEN from_id = ? THEN to_id ELSE from_id END as related_id - FROM graph_edges - WHERE from_id = ? OR to_id = ? - `).all(nodeId1, nodeId1, nodeId1) as { related_id: string }[]; - - expect(results.length).toBe(1); - expect(results[0].related_id).toBe(nodeId2); - }); - - it('should enforce unique constraint on from_id, to_id, edge_type', () => { - const now = new Date().toISOString(); - - db.prepare(` - INSERT INTO graph_edges (id, from_id, to_id, edge_type, weight, metadata, created_at) - VALUES (?, ?, ?, 'relates_to', 0.5, '{}', ?) - `).run(nanoid(), nodeId1, nodeId2, now); - - // Attempting to insert duplicate should fail - expect(() => { - db.prepare(` - INSERT INTO graph_edges (id, from_id, to_id, edge_type, weight, metadata, created_at) - VALUES (?, ?, ?, 'relates_to', 0.7, '{}', ?) - `).run(nanoid(), nodeId1, nodeId2, now); - }).toThrow(); - }); - }); - - describe('Decay Simulation', () => { - it('should be able to update retention strength', () => { - const id = nanoid(); - const now = new Date().toISOString(); - - // Insert a node with initial retention - db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, created_at, updated_at, last_accessed_at, - retention_strength, stability_factor, - source_type, source_platform, confidence, people, concepts, events, tags - ) VALUES (?, 'Test content', ?, ?, ?, 1.0, 1.0, 'manual', 'manual', 0.8, '[]', '[]', '[]', '[]') - `).run(id, now, now, now); - - // Simulate decay - const newRetention = 0.75; - db.prepare(` - UPDATE knowledge_nodes SET retention_strength = ? WHERE id = ? - `).run(newRetention, id); - - const result = db.prepare('SELECT retention_strength FROM knowledge_nodes WHERE id = ?').get(id) as { retention_strength: number }; - - expect(result.retention_strength).toBe(0.75); - }); - - it('should track review count', () => { - const id = nanoid(); - const now = new Date().toISOString(); - - db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, created_at, updated_at, last_accessed_at, - review_count, source_type, source_platform, confidence, people, concepts, events, tags - ) VALUES (?, 'Test content', ?, ?, ?, 0, 'manual', 'manual', 0.8, '[]', '[]', '[]', '[]') - `).run(id, now, now, now); - - // Simulate review - db.prepare(` - UPDATE knowledge_nodes - SET review_count = review_count + 1, - retention_strength = 1.0, - last_accessed_at = ? - WHERE id = ? - `).run(new Date().toISOString(), id); - - const result = db.prepare('SELECT review_count, retention_strength FROM knowledge_nodes WHERE id = ?').get(id) as { review_count: number; retention_strength: number }; - - expect(result.review_count).toBe(1); - expect(result.retention_strength).toBe(1.0); - }); - }); - - describe('Statistics', () => { - it('should count nodes correctly', () => { - const now = new Date().toISOString(); - - // Insert 3 nodes - for (let i = 0; i < 3; i++) { - db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, created_at, updated_at, last_accessed_at, - source_type, source_platform, confidence, people, concepts, events, tags - ) VALUES (?, ?, ?, ?, ?, 'manual', 'manual', 0.8, '[]', '[]', '[]', '[]') - `).run(nanoid(), `Node ${i}`, now, now, now); - } - - const result = db.prepare('SELECT COUNT(*) as count FROM knowledge_nodes').get() as { count: number }; - expect(result.count).toBe(3); - }); - - it('should count people correctly', () => { - const now = new Date().toISOString(); - - // Insert 2 people - for (let i = 0; i < 2; i++) { - db.prepare(` - INSERT INTO people (id, name, aliases, social_links, shared_topics, shared_projects, created_at, updated_at) - VALUES (?, ?, '[]', '{}', '[]', '[]', ?, ?) - `).run(nanoid(), `Person ${i}`, now, now); - } - - const result = db.prepare('SELECT COUNT(*) as count FROM people').get() as { count: number }; - expect(result.count).toBe(2); - }); - - it('should count edges correctly', () => { - const now = new Date().toISOString(); - - // Create nodes first - const nodeIds = [nanoid(), nanoid(), nanoid()]; - for (const id of nodeIds) { - db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, created_at, updated_at, last_accessed_at, - source_type, source_platform, confidence, people, concepts, events, tags - ) VALUES (?, 'Content', ?, ?, ?, 'manual', 'manual', 0.8, '[]', '[]', '[]', '[]') - `).run(id, now, now, now); - } - - // Insert 2 edges - db.prepare(` - INSERT INTO graph_edges (id, from_id, to_id, edge_type, weight, metadata, created_at) - VALUES (?, ?, ?, 'relates_to', 0.5, '{}', ?) - `).run(nanoid(), nodeIds[0], nodeIds[1], now); - - db.prepare(` - INSERT INTO graph_edges (id, from_id, to_id, edge_type, weight, metadata, created_at) - VALUES (?, ?, ?, 'supports', 0.7, '{}', ?) - `).run(nanoid(), nodeIds[1], nodeIds[2], now); - - const result = db.prepare('SELECT COUNT(*) as count FROM graph_edges').get() as { count: number }; - expect(result.count).toBe(2); - }); - }); -}); diff --git a/packages/core/src/__tests__/fsrs.test.ts b/packages/core/src/__tests__/fsrs.test.ts deleted file mode 100644 index fbfdaee..0000000 --- a/packages/core/src/__tests__/fsrs.test.ts +++ /dev/null @@ -1,560 +0,0 @@ -import { describe, it, expect } from '@rstest/core'; -import { - FSRSScheduler, - Grade, - FSRS_CONSTANTS, - initialDifficulty, - initialStability, - retrievability, - nextDifficulty, - nextRecallStability, - nextForgetStability, - nextInterval, - applySentimentBoost, - serializeFSRSState, - deserializeFSRSState, - optimalReviewTime, - isReviewDue, - type FSRSState, - type ReviewGrade, -} from '../core/fsrs.js'; - -describe('FSRS-5 Algorithm', () => { - describe('initialDifficulty', () => { - it('should return higher difficulty for Again grade', () => { - const dAgain = initialDifficulty(Grade.Again); - const dEasy = initialDifficulty(Grade.Easy); - expect(dAgain).toBeGreaterThan(dEasy); - }); - - it('should clamp difficulty between 1 and 10', () => { - const grades: ReviewGrade[] = [Grade.Again, Grade.Hard, Grade.Good, Grade.Easy]; - for (const grade of grades) { - const d = initialDifficulty(grade); - expect(d).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_DIFFICULTY); - expect(d).toBeLessThanOrEqual(FSRS_CONSTANTS.MAX_DIFFICULTY); - } - }); - - it('should return difficulty in order: Again > Hard > Good > Easy', () => { - const dAgain = initialDifficulty(Grade.Again); - const dHard = initialDifficulty(Grade.Hard); - const dGood = initialDifficulty(Grade.Good); - const dEasy = initialDifficulty(Grade.Easy); - - expect(dAgain).toBeGreaterThan(dHard); - expect(dHard).toBeGreaterThan(dGood); - expect(dGood).toBeGreaterThan(dEasy); - }); - }); - - describe('initialStability', () => { - it('should return positive stability for all grades', () => { - const grades: ReviewGrade[] = [Grade.Again, Grade.Hard, Grade.Good, Grade.Easy]; - for (const grade of grades) { - const s = initialStability(grade); - expect(s).toBeGreaterThan(0); - } - }); - - it('should return higher stability for easier grades', () => { - const sAgain = initialStability(Grade.Again); - const sEasy = initialStability(Grade.Easy); - expect(sEasy).toBeGreaterThan(sAgain); - }); - - it('should ensure minimum stability', () => { - const grades: ReviewGrade[] = [Grade.Again, Grade.Hard, Grade.Good, Grade.Easy]; - for (const grade of grades) { - const s = initialStability(grade); - expect(s).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_STABILITY); - } - }); - }); - - describe('retrievability', () => { - it('should return 1.0 when elapsed days is 0', () => { - const r = retrievability(10, 0); - expect(r).toBeCloseTo(1.0, 3); - }); - - it('should decay over time', () => { - const stability = 10; - const r0 = retrievability(stability, 0); - const r5 = retrievability(stability, 5); - const r30 = retrievability(stability, 30); - - expect(r0).toBeGreaterThan(r5); - expect(r5).toBeGreaterThan(r30); - }); - - it('should decay slower with higher stability', () => { - const elapsedDays = 10; - const rLowStability = retrievability(5, elapsedDays); - const rHighStability = retrievability(50, elapsedDays); - - expect(rHighStability).toBeGreaterThan(rLowStability); - }); - - it('should return 0 when stability is 0 or negative', () => { - expect(retrievability(0, 5)).toBe(0); - expect(retrievability(-1, 5)).toBe(0); - }); - - it('should return value between 0 and 1', () => { - const testCases = [ - { stability: 1, days: 100 }, - { stability: 100, days: 1 }, - { stability: 10, days: 10 }, - ]; - - for (const { stability, days } of testCases) { - const r = retrievability(stability, days); - expect(r).toBeGreaterThanOrEqual(0); - expect(r).toBeLessThanOrEqual(1); - } - }); - }); - - describe('nextDifficulty', () => { - it('should increase difficulty for Again grade', () => { - const currentD = 5; - const newD = nextDifficulty(currentD, Grade.Again); - expect(newD).toBeGreaterThan(currentD); - }); - - it('should decrease difficulty for Easy grade', () => { - const currentD = 5; - const newD = nextDifficulty(currentD, Grade.Easy); - expect(newD).toBeLessThan(currentD); - }); - - it('should keep difficulty within bounds', () => { - // Test at extremes - const lowD = nextDifficulty(FSRS_CONSTANTS.MIN_DIFFICULTY, Grade.Easy); - const highD = nextDifficulty(FSRS_CONSTANTS.MAX_DIFFICULTY, Grade.Again); - - expect(lowD).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_DIFFICULTY); - expect(highD).toBeLessThanOrEqual(FSRS_CONSTANTS.MAX_DIFFICULTY); - }); - }); - - describe('nextRecallStability', () => { - it('should increase stability after successful recall', () => { - const currentS = 10; - const difficulty = 5; - const r = 0.9; - - const newS = nextRecallStability(currentS, difficulty, r, Grade.Good); - expect(newS).toBeGreaterThan(currentS); - }); - - it('should give bigger boost for Easy grade', () => { - const currentS = 10; - const difficulty = 5; - const r = 0.9; - - const sGood = nextRecallStability(currentS, difficulty, r, Grade.Good); - const sEasy = nextRecallStability(currentS, difficulty, r, Grade.Easy); - - expect(sEasy).toBeGreaterThan(sGood); - }); - - it('should apply hard penalty for Hard grade', () => { - const currentS = 10; - const difficulty = 5; - const r = 0.9; - - const sGood = nextRecallStability(currentS, difficulty, r, Grade.Good); - const sHard = nextRecallStability(currentS, difficulty, r, Grade.Hard); - - expect(sHard).toBeLessThan(sGood); - }); - - it('should use forget stability for Again grade', () => { - const currentS = 10; - const difficulty = 5; - const r = 0.9; - - const sAgain = nextRecallStability(currentS, difficulty, r, Grade.Again); - - // Should call nextForgetStability internally, resulting in lower stability - expect(sAgain).toBeLessThan(currentS); - }); - }); - - describe('nextForgetStability', () => { - it('should return lower stability than current', () => { - const currentS = 10; - const difficulty = 5; - const r = 0.3; - - const newS = nextForgetStability(difficulty, currentS, r); - expect(newS).toBeLessThan(currentS); - }); - - it('should return positive stability', () => { - const newS = nextForgetStability(5, 10, 0.5); - expect(newS).toBeGreaterThan(0); - }); - - it('should keep stability within bounds', () => { - const newS = nextForgetStability(10, 100, 0.1); - expect(newS).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_STABILITY); - expect(newS).toBeLessThanOrEqual(FSRS_CONSTANTS.MAX_STABILITY); - }); - }); - - describe('nextInterval', () => { - it('should return 0 for 0 or negative stability', () => { - expect(nextInterval(0, 0.9)).toBe(0); - expect(nextInterval(-1, 0.9)).toBe(0); - }); - - it('should return longer intervals for higher stability', () => { - const iLow = nextInterval(5, 0.9); - const iHigh = nextInterval(50, 0.9); - - expect(iHigh).toBeGreaterThan(iLow); - }); - - it('should return shorter intervals for higher desired retention', () => { - const stability = 10; - const i90 = nextInterval(stability, 0.9); - const i95 = nextInterval(stability, 0.95); - - expect(i90).toBeGreaterThan(i95); - }); - - it('should return 0 for 100% retention', () => { - expect(nextInterval(10, 1.0)).toBe(0); - }); - - it('should return max interval for 0% retention', () => { - expect(nextInterval(10, 0)).toBe(FSRS_CONSTANTS.MAX_STABILITY); - }); - }); - - describe('applySentimentBoost', () => { - it('should not boost stability for neutral sentiment (0)', () => { - const stability = 10; - const boosted = applySentimentBoost(stability, 0, 2.0); - expect(boosted).toBe(stability); - }); - - it('should apply max boost for max sentiment (1)', () => { - const stability = 10; - const maxBoost = 2.0; - const boosted = applySentimentBoost(stability, 1, maxBoost); - expect(boosted).toBe(stability * maxBoost); - }); - - it('should apply proportional boost for intermediate sentiment', () => { - const stability = 10; - const maxBoost = 2.0; - const sentiment = 0.5; - const boosted = applySentimentBoost(stability, sentiment, maxBoost); - - // Expected: stability * (1 + (maxBoost - 1) * sentiment) = 10 * 1.5 = 15 - expect(boosted).toBe(15); - }); - - it('should clamp sentiment and maxBoost values', () => { - const stability = 10; - - // Sentiment should be clamped to 0-1 - const boosted1 = applySentimentBoost(stability, -0.5, 2.0); - expect(boosted1).toBe(stability); // Clamped to 0 - - // maxBoost should be clamped to 1-3 - const boosted2 = applySentimentBoost(stability, 1, 5.0); - expect(boosted2).toBe(stability * 3); // Clamped to 3 - }); - }); -}); - -describe('FSRSScheduler', () => { - describe('constructor', () => { - it('should create scheduler with default config', () => { - const scheduler = new FSRSScheduler(); - const config = scheduler.getConfig(); - - expect(config.desiredRetention).toBe(0.9); - expect(config.maximumInterval).toBe(36500); - expect(config.enableSentimentBoost).toBe(true); - expect(config.maxSentimentBoost).toBe(2); - }); - - it('should accept custom config', () => { - const scheduler = new FSRSScheduler({ - desiredRetention: 0.85, - maximumInterval: 365, - enableSentimentBoost: false, - maxSentimentBoost: 1.5, - }); - const config = scheduler.getConfig(); - - expect(config.desiredRetention).toBe(0.85); - expect(config.maximumInterval).toBe(365); - expect(config.enableSentimentBoost).toBe(false); - expect(config.maxSentimentBoost).toBe(1.5); - }); - }); - - describe('newCard', () => { - it('should create new card with initial state', () => { - const scheduler = new FSRSScheduler(); - const state = scheduler.newCard(); - - expect(state.state).toBe('New'); - expect(state.reps).toBe(0); - expect(state.lapses).toBe(0); - expect(state.difficulty).toBeGreaterThanOrEqual(FSRS_CONSTANTS.MIN_DIFFICULTY); - expect(state.difficulty).toBeLessThanOrEqual(FSRS_CONSTANTS.MAX_DIFFICULTY); - expect(state.stability).toBeGreaterThan(0); - expect(state.scheduledDays).toBe(0); - }); - }); - - describe('review', () => { - it('should handle new item review', () => { - const scheduler = new FSRSScheduler(); - const state = scheduler.newCard(); - - const result = scheduler.review(state, Grade.Good, 0); - - expect(result.state.stability).toBeGreaterThan(0); - expect(result.state.reps).toBe(1); - expect(result.state.state).not.toBe('New'); - expect(result.interval).toBeGreaterThanOrEqual(0); - expect(result.isLapse).toBe(false); - }); - - it('should handle Again grade as lapse for reviewed cards', () => { - const scheduler = new FSRSScheduler(); - let state = scheduler.newCard(); - - // First review to move out of New state - const result1 = scheduler.review(state, Grade.Good, 0); - state = result1.state; - - // Second review with Again (lapse) - const result2 = scheduler.review(state, Grade.Again, 1); - - expect(result2.isLapse).toBe(true); - expect(result2.state.lapses).toBe(1); - expect(result2.state.state).toBe('Relearning'); - }); - - it('should apply sentiment boost when enabled', () => { - const scheduler = new FSRSScheduler({ enableSentimentBoost: true, maxSentimentBoost: 2 }); - const state = scheduler.newCard(); - - const resultNoBoost = scheduler.review(state, Grade.Good, 0, 0); - const resultWithBoost = scheduler.review(state, Grade.Good, 0, 1); - - expect(resultWithBoost.state.stability).toBeGreaterThan(resultNoBoost.state.stability); - }); - - it('should not apply sentiment boost when disabled', () => { - const scheduler = new FSRSScheduler({ enableSentimentBoost: false }); - const state = scheduler.newCard(); - - const resultNoBoost = scheduler.review(state, Grade.Good, 0, 0); - const resultWithBoost = scheduler.review(state, Grade.Good, 0, 1); - - // Stability should be the same since boost is disabled - expect(resultWithBoost.state.stability).toBe(resultNoBoost.state.stability); - }); - - it('should respect maximum interval', () => { - const maxInterval = 30; - const scheduler = new FSRSScheduler({ maximumInterval: maxInterval }); - const state = scheduler.newCard(); - - // Review multiple times to build up stability - let currentState = state; - for (let i = 0; i < 10; i++) { - const result = scheduler.review(currentState, Grade.Easy, 0); - expect(result.interval).toBeLessThanOrEqual(maxInterval); - currentState = result.state; - } - }); - }); - - describe('getRetrievability', () => { - it('should return 1.0 for just-reviewed card', () => { - const scheduler = new FSRSScheduler(); - const state = scheduler.newCard(); - state.lastReview = new Date(); - - const r = scheduler.getRetrievability(state, 0); - expect(r).toBeCloseTo(1.0, 3); - }); - - it('should return lower value after time passes', () => { - const scheduler = new FSRSScheduler(); - const state = scheduler.newCard(); - - const r0 = scheduler.getRetrievability(state, 0); - const r10 = scheduler.getRetrievability(state, 10); - - expect(r0).toBeGreaterThan(r10); - }); - }); - - describe('previewReviews', () => { - it('should return results for all grades', () => { - const scheduler = new FSRSScheduler(); - const state = scheduler.newCard(); - - const preview = scheduler.previewReviews(state, 0); - - expect(preview.again).toBeDefined(); - expect(preview.hard).toBeDefined(); - expect(preview.good).toBeDefined(); - expect(preview.easy).toBeDefined(); - }); - - it('should show increasing intervals from again to easy', () => { - const scheduler = new FSRSScheduler(); - let state = scheduler.newCard(); - - // First review to establish some stability - const result = scheduler.review(state, Grade.Good, 0); - state = result.state; - - const preview = scheduler.previewReviews(state, 1); - - // Generally, easy should have longest interval, again shortest - expect(preview.easy.interval).toBeGreaterThanOrEqual(preview.good.interval); - expect(preview.good.interval).toBeGreaterThanOrEqual(preview.hard.interval); - }); - }); -}); - -describe('FSRS Utility Functions', () => { - describe('serializeFSRSState / deserializeFSRSState', () => { - it('should serialize and deserialize state correctly', () => { - const scheduler = new FSRSScheduler(); - const state = scheduler.newCard(); - - const serialized = serializeFSRSState(state); - const deserialized = deserializeFSRSState(serialized); - - expect(deserialized.difficulty).toBe(state.difficulty); - expect(deserialized.stability).toBe(state.stability); - expect(deserialized.state).toBe(state.state); - expect(deserialized.reps).toBe(state.reps); - expect(deserialized.lapses).toBe(state.lapses); - expect(deserialized.scheduledDays).toBe(state.scheduledDays); - }); - - it('should preserve lastReview date', () => { - const state: FSRSState = { - difficulty: 5, - stability: 10, - state: 'Review', - reps: 5, - lapses: 1, - lastReview: new Date('2024-01-15T12:00:00Z'), - scheduledDays: 7, - }; - - const serialized = serializeFSRSState(state); - const deserialized = deserializeFSRSState(serialized); - - expect(deserialized.lastReview.toISOString()).toBe(state.lastReview.toISOString()); - }); - }); - - describe('optimalReviewTime', () => { - it('should return interval based on stability', () => { - const state: FSRSState = { - difficulty: 5, - stability: 10, - state: 'Review', - reps: 3, - lapses: 0, - lastReview: new Date(), - scheduledDays: 7, - }; - - const interval = optimalReviewTime(state, 0.9); - expect(interval).toBeGreaterThan(0); - }); - - it('should return shorter interval for higher retention target', () => { - const state: FSRSState = { - difficulty: 5, - stability: 10, - state: 'Review', - reps: 3, - lapses: 0, - lastReview: new Date(), - scheduledDays: 7, - }; - - const i90 = optimalReviewTime(state, 0.9); - const i95 = optimalReviewTime(state, 0.95); - - expect(i90).toBeGreaterThan(i95); - }); - }); - - describe('isReviewDue', () => { - it('should return false for just-created card', () => { - const state: FSRSState = { - difficulty: 5, - stability: 10, - state: 'Review', - reps: 3, - lapses: 0, - lastReview: new Date(), - scheduledDays: 7, - }; - - expect(isReviewDue(state)).toBe(false); - }); - - it('should return true when scheduled days have passed', () => { - const pastDate = new Date(); - pastDate.setDate(pastDate.getDate() - 10); - - const state: FSRSState = { - difficulty: 5, - stability: 10, - state: 'Review', - reps: 3, - lapses: 0, - lastReview: pastDate, - scheduledDays: 7, - }; - - expect(isReviewDue(state)).toBe(true); - }); - - it('should use retention threshold when provided', () => { - const pastDate = new Date(); - pastDate.setDate(pastDate.getDate() - 5); - - const state: FSRSState = { - difficulty: 5, - stability: 10, - state: 'Review', - reps: 3, - lapses: 0, - lastReview: pastDate, - scheduledDays: 30, // Not due by scheduledDays - }; - - // Check with high retention threshold (should be due) - const isDueHighThreshold = isReviewDue(state, 0.95); - // Check with low retention threshold (might not be due) - const isDueLowThreshold = isReviewDue(state, 0.5); - - // With higher threshold, more likely to be due - expect(isDueHighThreshold || !isDueLowThreshold).toBe(true); - }); - }); -}); diff --git a/packages/core/src/__tests__/integration/mcp-tools.test.ts b/packages/core/src/__tests__/integration/mcp-tools.test.ts deleted file mode 100644 index b89fb9e..0000000 --- a/packages/core/src/__tests__/integration/mcp-tools.test.ts +++ /dev/null @@ -1,1333 +0,0 @@ -/** - * Integration tests for all 14 MCP tools in Vestige MCP - * - * Tests cover the complete tool functionality including: - * - Input validation - * - Database operations - * - Response formatting - * - Edge cases and error handling - */ - -import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@rstest/core'; -import { VestigeDatabase } from '../../core/database.js'; -import type { KnowledgeNode, PersonNode } from '../../core/types.js'; - -/** - * Creates an in-memory test database instance - */ -function createTestDatabase(): VestigeDatabase { - return new VestigeDatabase(':memory:'); -} - -/** - * Create a mock timestamp for consistent testing - */ -function mockTimestamp(daysAgo: number = 0): Date { - const date = new Date(); - date.setDate(date.getDate() - daysAgo); - return date; -} - -// ============================================================================ -// MCP TOOL HANDLER MOCK -// ============================================================================ - -/** - * Creates mock MCP tool handlers that simulate the actual tool behavior - * These handlers call the same database methods as the real MCP server - */ -function createMCPToolHandler(db: VestigeDatabase) { - return { - // --- Tool 1: ingest --- - async ingest(args: { - content: string; - source?: string; - platform?: string; - sourceId?: string; - sourceUrl?: string; - timestamp?: string; - people?: string[]; - tags?: string[]; - }) { - const node = db.insertNode({ - content: args.content, - sourceType: (args.source as KnowledgeNode['sourceType']) || 'manual', - sourcePlatform: (args.platform as KnowledgeNode['sourcePlatform']) || 'manual', - sourceId: args.sourceId, - sourceUrl: args.sourceUrl, - createdAt: args.timestamp ? new Date(args.timestamp) : new Date(), - updatedAt: new Date(), - lastAccessedAt: new Date(), - accessCount: 0, - retentionStrength: 1.0, - stabilityFactor: 1.0, - reviewCount: 0, - confidence: 0.8, - isContradicted: false, - contradictionIds: [], - people: args.people || [], - concepts: [], - events: [], - tags: args.tags || [], - sourceChain: [], - }); - - return { - success: true, - nodeId: node.id, - message: `Knowledge ingested successfully. Node ID: ${node.id}`, - }; - }, - - // --- Tool 2: recall --- - async recall(args: { query: string; limit?: number; offset?: number }) { - const result = db.searchNodes(args.query, { - limit: args.limit || 10, - offset: args.offset || 0, - }); - - // Update access timestamps for retrieved nodes - for (const node of result.items) { - try { - db.updateNodeAccess(node.id); - } catch { - // Ignore access update errors - } - } - - const formatted = result.items.map((node) => ({ - id: node.id, - content: node.content, - summary: node.summary, - source: { - type: node.sourceType, - platform: node.sourcePlatform, - url: node.sourceUrl, - }, - metadata: { - createdAt: node.createdAt.toISOString(), - lastAccessed: node.lastAccessedAt.toISOString(), - retentionStrength: node.retentionStrength, - sentimentIntensity: node.sentimentIntensity, - confidence: node.confidence, - }, - gitContext: node.gitContext, - people: node.people, - tags: node.tags, - })); - - return { - query: args.query, - total: result.total, - showing: result.items.length, - offset: result.offset, - hasMore: result.hasMore, - results: formatted, - }; - }, - - // --- Tool 3: get_knowledge --- - async getKnowledge(args: { nodeId: string }) { - const node = db.getNode(args.nodeId); - if (!node) { - return { error: 'Node not found', nodeId: args.nodeId }; - } - - db.updateNodeAccess(args.nodeId); - return node; - }, - - // --- Tool 4: get_related --- - async getRelated(args: { nodeId: string; depth?: number }) { - const depth = args.depth || 1; - const relatedIds = db.getRelatedNodes(args.nodeId, depth); - const relatedNodes = relatedIds - .map((id) => db.getNode(id)) - .filter((n): n is KnowledgeNode => n !== null); - - return { - sourceNode: args.nodeId, - depth, - relatedCount: relatedNodes.length, - related: relatedNodes.map((n) => ({ - id: n.id, - summary: n.summary || n.content.slice(0, 200), - tags: n.tags, - })), - }; - }, - - // --- Tool 5: remember_person --- - async rememberPerson(args: { - name: string; - howWeMet?: string; - relationshipType?: string; - organization?: string; - role?: string; - email?: string; - notes?: string; - sharedTopics?: string[]; - }) { - // Check if person exists - const existing = db.getPersonByName(args.name); - if (existing) { - return { - message: `Person "${args.name}" already exists`, - personId: existing.id, - existing: true, - }; - } - - const person = db.insertPerson({ - name: args.name, - aliases: [], - howWeMet: args.howWeMet, - relationshipType: args.relationshipType, - organization: args.organization, - role: args.role, - email: args.email, - notes: args.notes, - sharedTopics: args.sharedTopics || [], - sharedProjects: [], - socialLinks: {}, - contactFrequency: 0, - relationshipHealth: 0.5, - createdAt: new Date(), - updatedAt: new Date(), - }); - - return { - success: true, - personId: person.id, - message: `Remembered ${args.name}`, - }; - }, - - // --- Tool 6: get_person --- - async getPerson(args: { name: string }) { - const person = db.getPersonByName(args.name); - if (!person) { - return { - found: false, - message: `No person named "${args.name}" found in memory`, - }; - } - - const daysSinceContact = person.lastContactAt - ? Math.floor( - (Date.now() - person.lastContactAt.getTime()) / (1000 * 60 * 60 * 24) - ) - : null; - - return { - found: true, - person: { - ...person, - daysSinceContact, - }, - }; - }, - - // --- Tool 7: mark_reviewed --- - async markReviewed(args: { nodeId: string }) { - const nodeBefore = db.getNode(args.nodeId); - if (!nodeBefore) { - return { error: 'Node not found' }; - } - - db.markReviewed(args.nodeId); - - const nodeAfter = db.getNode(args.nodeId); - - return { - success: true, - nodeId: args.nodeId, - previousRetention: nodeBefore.retentionStrength, - newRetention: nodeAfter?.retentionStrength, - previousStability: nodeBefore.stabilityFactor, - newStability: nodeAfter?.stabilityFactor, - reviewCount: nodeAfter?.reviewCount, - nextReviewDate: nodeAfter?.nextReviewDate?.toISOString(), - message: 'Memory reinforced', - }; - }, - - // --- Tool 8: daily_brief --- - async dailyBrief() { - const stats = db.getStats(); - const health = db.checkHealth(); - const decaying = db.getDecayingNodes(0.5, { limit: 5 }); - const reconnect = db.getPeopleToReconnect(30, { limit: 5 }); - const recent = db.getRecentNodes({ limit: 5 }); - - const getTimeBasedGreeting = (): string => { - const hour = new Date().getHours(); - if (hour < 12) return 'Good morning'; - if (hour < 17) return 'Good afternoon'; - return 'Good evening'; - }; - - return { - date: new Date().toISOString().split('T')[0], - greeting: getTimeBasedGreeting(), - healthStatus: health.status, - warnings: health.warnings.length > 0 ? health.warnings : undefined, - stats: { - totalKnowledge: stats.totalNodes, - peopleInNetwork: stats.totalPeople, - connections: stats.totalEdges, - databaseSize: db.getDatabaseSize().formatted, - }, - reviewNeeded: decaying.items.map((n) => ({ - id: n.id, - preview: n.summary || n.content.slice(0, 100), - retentionStrength: n.retentionStrength, - daysSinceAccess: Math.floor( - (Date.now() - n.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24) - ), - })), - peopleToReconnect: reconnect.items.map((p) => ({ - name: p.name, - daysSinceContact: p.lastContactAt - ? Math.floor( - (Date.now() - p.lastContactAt.getTime()) / (1000 * 60 * 60 * 24) - ) - : null, - sharedTopics: p.sharedTopics, - })), - recentlyAdded: recent.items.map((n) => ({ - id: n.id, - preview: n.summary || n.content.slice(0, 100), - source: n.sourcePlatform, - })), - }; - }, - - // --- Tool 9: health_check --- - async healthCheck() { - const health = db.checkHealth(); - const size = db.getDatabaseSize(); - - const getHealthRecommendations = (): string[] => { - const recommendations: string[] = []; - - if (health.status === 'critical') { - recommendations.push( - 'CRITICAL: Immediate attention required. Check warnings for details.' - ); - } - - if (!health.lastBackup) { - recommendations.push('Create your first backup using the backup tool'); - } else { - const daysSinceBackup = - (Date.now() - new Date(health.lastBackup).getTime()) / - (1000 * 60 * 60 * 24); - if (daysSinceBackup > 7) { - recommendations.push( - `Consider creating a backup (last backup was ${Math.floor(daysSinceBackup)} days ago)` - ); - } - } - - if (health.dbSizeMB > 50) { - recommendations.push('Consider running optimize_database to reclaim space'); - } - - if (health.nodeCount > 10000) { - recommendations.push('Large knowledge base detected. Searches may be slower.'); - } - - if (recommendations.length === 0) { - recommendations.push('Everything looks healthy!'); - } - - return recommendations; - }; - - return { - ...health, - databaseSize: size, - recommendations: getHealthRecommendations(), - }; - }, - - // --- Tool 10: backup --- - async backup() { - // Note: For in-memory databases, backup operations are not supported - // as there's no file to copy. This mock handles that gracefully. - try { - const backupPath = db.backup(); - const backups = db.listBackups(); - - return { - success: true, - backupPath, - message: 'Backup created successfully', - totalBackups: backups.length, - backups: backups.slice(0, 5).map((b) => ({ - path: b.path, - size: `${(b.size / 1024 / 1024).toFixed(2)}MB`, - date: b.date.toISOString(), - })), - }; - } catch { - // In-memory databases cannot be backed up - this is expected - return { - success: false, - message: 'Backup not supported for in-memory databases', - totalBackups: 0, - backups: [], - }; - } - }, - - // --- Tool 11: list_backups --- - async listBackups() { - const backups = db.listBackups(); - - return { - totalBackups: backups.length, - backups: backups.map((b) => ({ - path: b.path, - size: `${(b.size / 1024 / 1024).toFixed(2)}MB`, - date: b.date.toISOString(), - })), - }; - }, - - // --- Tool 12: optimize_database --- - async optimizeDatabase() { - const sizeBefore = db.getDatabaseSize(); - db.optimize(); - const sizeAfter = db.getDatabaseSize(); - - return { - success: true, - message: 'Database optimized', - sizeBefore: sizeBefore.formatted, - sizeAfter: sizeAfter.formatted, - spaceSaved: `${(sizeBefore.mb - sizeAfter.mb).toFixed(2)}MB`, - }; - }, - - // --- Tool 13: apply_decay --- - async applyDecay() { - const updatedCount = db.applyDecay(); - - return { - success: true, - nodesUpdated: updatedCount, - message: `Applied decay to ${updatedCount} knowledge nodes`, - }; - }, - }; -} - -// ============================================================================ -// TEST SUITES -// ============================================================================ - -describe('MCP Tools Integration', () => { - let db: VestigeDatabase; - let tools: ReturnType; - - beforeAll(() => { - db = createTestDatabase(); - tools = createMCPToolHandler(db); - }); - - afterAll(() => { - db.close(); - }); - - // ========================================================================== - // Tool 1: ingest - // ========================================================================== - describe('ingest tool', () => { - it('should store content and return node ID', async () => { - const result = await tools.ingest({ - content: 'Test knowledge for MCP integration', - }); - - expect(result.success).toBe(true); - expect(result.nodeId).toBeDefined(); - expect(typeof result.nodeId).toBe('string'); - expect(result.message).toContain('Knowledge ingested successfully'); - }); - - it('should store content with tags', async () => { - const result = await tools.ingest({ - content: 'Tagged content for testing', - tags: ['test', 'mcp', 'integration'], - }); - - expect(result.success).toBe(true); - - const node = db.getNode(result.nodeId); - expect(node).not.toBeNull(); - expect(node?.tags).toContain('test'); - expect(node?.tags).toContain('mcp'); - expect(node?.tags).toContain('integration'); - }); - - it('should store content with people references', async () => { - const result = await tools.ingest({ - content: 'Meeting notes with team members', - people: ['Alice', 'Bob', 'Charlie'], - }); - - expect(result.success).toBe(true); - - const node = db.getNode(result.nodeId); - expect(node?.people).toContain('Alice'); - expect(node?.people).toContain('Bob'); - expect(node?.people).toContain('Charlie'); - }); - - it('should use specified source type and platform', async () => { - const result = await tools.ingest({ - content: 'Article about TypeScript patterns', - source: 'article', - platform: 'browser', - sourceUrl: 'https://example.com/article', - }); - - const node = db.getNode(result.nodeId); - expect(node?.sourceType).toBe('article'); - expect(node?.sourcePlatform).toBe('browser'); - expect(node?.sourceUrl).toBe('https://example.com/article'); - }); - - it('should use custom timestamp when provided', async () => { - const customDate = '2024-01-15T10:30:00.000Z'; - const result = await tools.ingest({ - content: 'Historical note', - timestamp: customDate, - }); - - const node = db.getNode(result.nodeId); - expect(node?.createdAt.toISOString()).toBe(customDate); - }); - - it('should initialize with correct default values', async () => { - const result = await tools.ingest({ - content: 'Testing default values', - }); - - const node = db.getNode(result.nodeId); - expect(node?.retentionStrength).toBe(1.0); - expect(node?.stabilityFactor).toBe(1.0); - expect(node?.confidence).toBe(0.8); - expect(node?.accessCount).toBe(0); - expect(node?.reviewCount).toBe(0); - expect(node?.isContradicted).toBe(false); - }); - }); - - // ========================================================================== - // Tool 2: recall - // ========================================================================== - describe('recall tool', () => { - beforeEach(async () => { - // Seed some test data for search tests - await tools.ingest({ content: 'React hooks tutorial with useState examples' }); - await tools.ingest({ content: 'Vue composition API patterns' }); - await tools.ingest({ content: 'React context for global state management' }); - await tools.ingest({ content: 'Angular dependency injection guide' }); - }); - - it('should find content by keyword', async () => { - const result = await tools.recall({ query: 'React' }); - - expect(result.total).toBeGreaterThanOrEqual(2); - expect(result.results.length).toBeGreaterThanOrEqual(2); - expect(result.results.every((n) => n.content.includes('React'))).toBe(true); - }); - - it('should respect limit parameter', async () => { - const result = await tools.recall({ query: 'React', limit: 1 }); - - expect(result.results.length).toBe(1); - expect(result.hasMore).toBe(true); - }); - - it('should support pagination with offset', async () => { - const page1 = await tools.recall({ query: 'React', limit: 1, offset: 0 }); - const page2 = await tools.recall({ query: 'React', limit: 1, offset: 1 }); - - expect(page1.results[0].id).not.toBe(page2.results[0]?.id); - expect(page1.offset).toBe(0); - expect(page2.offset).toBe(1); - }); - - it('should return empty results for non-matching query', async () => { - const result = await tools.recall({ query: 'xyznonexistent123' }); - - expect(result.total).toBe(0); - expect(result.results.length).toBe(0); - expect(result.hasMore).toBe(false); - }); - - it('should update access count on retrieve', async () => { - // Create a node and recall it - const ingestResult = await tools.ingest({ - content: 'Unique searchable content xyz123', - }); - - const nodeBefore = db.getNode(ingestResult.nodeId); - expect(nodeBefore?.accessCount).toBe(0); - - await tools.recall({ query: 'xyz123' }); - - const nodeAfter = db.getNode(ingestResult.nodeId); - expect(nodeAfter?.accessCount).toBe(1); - }); - - it('should include metadata in results', async () => { - await tools.ingest({ - content: 'Content with full metadata test123', - tags: ['metadata', 'test'], - }); - - const result = await tools.recall({ query: 'metadata test123' }); - - expect(result.results.length).toBeGreaterThanOrEqual(1); - const firstResult = result.results[0]; - expect(firstResult.metadata).toBeDefined(); - expect(firstResult.metadata.createdAt).toBeDefined(); - expect(firstResult.metadata.retentionStrength).toBeDefined(); - expect(firstResult.tags).toContain('metadata'); - }); - }); - - // ========================================================================== - // Tool 3: get_knowledge - // ========================================================================== - describe('get_knowledge tool', () => { - it('should retrieve existing node by ID', async () => { - const ingestResult = await tools.ingest({ - content: 'Specific node for get_knowledge test', - tags: ['getknowledge', 'test'], - }); - - const result = await tools.getKnowledge({ nodeId: ingestResult.nodeId }); - - expect(result).not.toHaveProperty('error'); - expect((result as KnowledgeNode).id).toBe(ingestResult.nodeId); - expect((result as KnowledgeNode).content).toBe('Specific node for get_knowledge test'); - expect((result as KnowledgeNode).tags).toContain('getknowledge'); - }); - - it('should return error for non-existent node', async () => { - const result = await tools.getKnowledge({ nodeId: 'nonexistent-id-12345' }); - - expect(result).toHaveProperty('error'); - expect((result as { error: string }).error).toBe('Node not found'); - }); - - it('should update access count when retrieving', async () => { - const ingestResult = await tools.ingest({ - content: 'Node for access count test', - }); - - const nodeBefore = db.getNode(ingestResult.nodeId); - expect(nodeBefore?.accessCount).toBe(0); - - await tools.getKnowledge({ nodeId: ingestResult.nodeId }); - - const nodeAfter = db.getNode(ingestResult.nodeId); - expect(nodeAfter?.accessCount).toBe(1); - }); - - it('should update last accessed timestamp', async () => { - const ingestResult = await tools.ingest({ - content: 'Node for timestamp test', - }); - - const nodeBefore = db.getNode(ingestResult.nodeId); - const initialAccessTime = nodeBefore?.lastAccessedAt.getTime() || 0; - - // Wait a tiny bit to ensure timestamp difference - await new Promise((resolve) => setTimeout(resolve, 10)); - - await tools.getKnowledge({ nodeId: ingestResult.nodeId }); - - const nodeAfter = db.getNode(ingestResult.nodeId); - expect(nodeAfter?.lastAccessedAt.getTime()).toBeGreaterThan(initialAccessTime); - }); - }); - - // ========================================================================== - // Tool 4: get_related - // ========================================================================== - describe('get_related tool', () => { - it('should return empty when no connections exist', async () => { - const ingestResult = await tools.ingest({ - content: 'Isolated node with no connections', - }); - - const result = await tools.getRelated({ nodeId: ingestResult.nodeId }); - - expect(result.sourceNode).toBe(ingestResult.nodeId); - expect(result.relatedCount).toBe(0); - expect(result.related).toHaveLength(0); - }); - - it('should find directly connected nodes (depth 1)', async () => { - // Create two nodes and connect them - const node1 = await tools.ingest({ content: 'Node A for graph test' }); - const node2 = await tools.ingest({ content: 'Node B for graph test' }); - - // Create an edge between them - db.insertEdge({ - fromId: node1.nodeId, - toId: node2.nodeId, - edgeType: 'relates_to', - weight: 0.8, - }); - - const result = await tools.getRelated({ nodeId: node1.nodeId, depth: 1 }); - - expect(result.depth).toBe(1); - expect(result.relatedCount).toBe(1); - expect(result.related[0].id).toBe(node2.nodeId); - }); - - it('should traverse multiple hops with depth > 1', async () => { - // Create a chain: A -> B -> C - const nodeA = await tools.ingest({ content: 'Node A chain' }); - const nodeB = await tools.ingest({ content: 'Node B chain' }); - const nodeC = await tools.ingest({ content: 'Node C chain' }); - - db.insertEdge({ - fromId: nodeA.nodeId, - toId: nodeB.nodeId, - edgeType: 'relates_to', - }); - db.insertEdge({ - fromId: nodeB.nodeId, - toId: nodeC.nodeId, - edgeType: 'relates_to', - }); - - // Depth 1 should only find B - const depth1Result = await tools.getRelated({ nodeId: nodeA.nodeId, depth: 1 }); - expect(depth1Result.relatedCount).toBe(1); - - // Depth 2 should find both B and C - const depth2Result = await tools.getRelated({ nodeId: nodeA.nodeId, depth: 2 }); - expect(depth2Result.relatedCount).toBe(2); - }); - - it('should use default depth of 1', async () => { - const node1 = await tools.ingest({ content: 'Default depth test A' }); - const node2 = await tools.ingest({ content: 'Default depth test B' }); - - db.insertEdge({ - fromId: node1.nodeId, - toId: node2.nodeId, - edgeType: 'relates_to', - }); - - const result = await tools.getRelated({ nodeId: node1.nodeId }); - - expect(result.depth).toBe(1); - }); - }); - - // ========================================================================== - // Tool 5: remember_person - // ========================================================================== - describe('remember_person tool', () => { - it('should create a new person with basic info', async () => { - const result = await tools.rememberPerson({ - name: 'John Smith', - }); - - expect(result.success).toBe(true); - expect(result.personId).toBeDefined(); - expect(result.message).toContain('Remembered John Smith'); - }); - - it('should create a person with all fields', async () => { - const result = await tools.rememberPerson({ - name: 'Jane Doe', - howWeMet: 'Tech conference', - relationshipType: 'colleague', - organization: 'TechCorp', - role: 'Senior Developer', - email: 'jane@techcorp.com', - notes: 'Expert in distributed systems', - sharedTopics: ['microservices', 'kubernetes'], - }); - - expect(result.success).toBe(true); - - const person = db.getPersonByName('Jane Doe'); - expect(person?.howWeMet).toBe('Tech conference'); - expect(person?.relationshipType).toBe('colleague'); - expect(person?.organization).toBe('TechCorp'); - expect(person?.role).toBe('Senior Developer'); - expect(person?.email).toBe('jane@techcorp.com'); - expect(person?.notes).toBe('Expert in distributed systems'); - expect(person?.sharedTopics).toContain('microservices'); - }); - - it('should detect duplicate person', async () => { - await tools.rememberPerson({ name: 'Duplicate Person' }); - const result = await tools.rememberPerson({ name: 'Duplicate Person' }); - - expect(result.existing).toBe(true); - expect(result.message).toContain('already exists'); - }); - - it('should initialize default values correctly', async () => { - const result = await tools.rememberPerson({ name: 'Default Values Person' }); - const person = db.getPerson(result.personId!); - - expect(person?.relationshipHealth).toBe(0.5); - expect(person?.contactFrequency).toBe(0); - expect(person?.sharedTopics).toHaveLength(0); - expect(person?.aliases).toHaveLength(0); - }); - }); - - // ========================================================================== - // Tool 6: get_person - // ========================================================================== - describe('get_person tool', () => { - beforeEach(async () => { - // Create a test person with last contact date - const person = db.insertPerson({ - name: 'Test Person For Lookup', - aliases: ['TP'], - howWeMet: 'Test setup', - sharedTopics: ['testing'], - sharedProjects: [], - socialLinks: {}, - contactFrequency: 0, - relationshipHealth: 0.6, - lastContactAt: mockTimestamp(15), // 15 days ago - createdAt: new Date(), - updatedAt: new Date(), - }); - }); - - it('should find person by name', async () => { - const result = await tools.getPerson({ name: 'Test Person For Lookup' }); - - expect(result.found).toBe(true); - expect(result.person?.name).toBe('Test Person For Lookup'); - }); - - it('should calculate days since contact', async () => { - const result = await tools.getPerson({ name: 'Test Person For Lookup' }); - - expect(result.found).toBe(true); - // Should be approximately 15 days - expect(result.person?.daysSinceContact).toBeGreaterThanOrEqual(14); - expect(result.person?.daysSinceContact).toBeLessThanOrEqual(16); - }); - - it('should return not found for non-existent person', async () => { - const result = await tools.getPerson({ name: 'Non Existent Person' }); - - expect(result.found).toBe(false); - // Message contains "found in memory" (not found in memory) - expect(result.message).toContain('found in memory'); - }); - - it('should handle person with no last contact date', async () => { - db.insertPerson({ - name: 'No Contact Person', - aliases: [], - sharedTopics: [], - sharedProjects: [], - socialLinks: {}, - contactFrequency: 0, - relationshipHealth: 0.5, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const result = await tools.getPerson({ name: 'No Contact Person' }); - - expect(result.found).toBe(true); - expect(result.person?.daysSinceContact).toBeNull(); - }); - }); - - // ========================================================================== - // Tool 7: mark_reviewed - // ========================================================================== - describe('mark_reviewed tool', () => { - it('should update retention strength', async () => { - const ingestResult = await tools.ingest({ - content: 'Node for review test', - }); - - // Manually decrease retention to simulate decay - db['db'] - .prepare('UPDATE knowledge_nodes SET retention_strength = 0.5 WHERE id = ?') - .run(ingestResult.nodeId); - - const result = await tools.markReviewed({ nodeId: ingestResult.nodeId }); - - expect(result.success).toBe(true); - expect(result.previousRetention).toBe(0.5); - expect(result.newRetention).toBe(1.0); // Should reset to 1.0 - }); - - it('should increase stability factor on successful review', async () => { - const ingestResult = await tools.ingest({ - content: 'Node for stability test', - }); - - const nodeBefore = db.getNode(ingestResult.nodeId); - const initialStability = nodeBefore?.stabilityFactor || 1.0; - - const result = await tools.markReviewed({ nodeId: ingestResult.nodeId }); - - expect(result.newStability).toBeGreaterThan(initialStability); - }); - - it('should increment review count', async () => { - const ingestResult = await tools.ingest({ - content: 'Node for review count test', - }); - - await tools.markReviewed({ nodeId: ingestResult.nodeId }); - const result = await tools.markReviewed({ nodeId: ingestResult.nodeId }); - - expect(result.reviewCount).toBe(2); - }); - - it('should schedule next review date', async () => { - const ingestResult = await tools.ingest({ - content: 'Node for next review test', - }); - - const result = await tools.markReviewed({ nodeId: ingestResult.nodeId }); - - expect(result.nextReviewDate).toBeDefined(); - const nextReview = new Date(result.nextReviewDate!); - expect(nextReview.getTime()).toBeGreaterThan(Date.now()); - }); - - it('should return error for non-existent node', async () => { - const result = await tools.markReviewed({ nodeId: 'nonexistent-node-id' }); - - expect(result.error).toBe('Node not found'); - }); - - it('should reset stability on lapse (low retention)', async () => { - const ingestResult = await tools.ingest({ - content: 'Node for lapse test', - }); - - // Simulate a highly stable node that then decays below threshold - db['db'] - .prepare( - 'UPDATE knowledge_nodes SET stability_factor = 10, retention_strength = 0.2 WHERE id = ?' - ) - .run(ingestResult.nodeId); - - const result = await tools.markReviewed({ nodeId: ingestResult.nodeId }); - - // Stability should reset to 1.0 on lapse - expect(result.newStability).toBe(1.0); - }); - }); - - // ========================================================================== - // Tool 8: daily_brief - // ========================================================================== - describe('daily_brief tool', () => { - beforeEach(async () => { - // Seed some data for the brief - await tools.ingest({ content: 'Recent knowledge 1', tags: ['brief'] }); - await tools.ingest({ content: 'Recent knowledge 2', tags: ['brief'] }); - await tools.rememberPerson({ - name: 'Brief Test Person', - sharedTopics: ['testing'], - }); - }); - - it('should return all required sections', async () => { - const result = await tools.dailyBrief(); - - expect(result.date).toBeDefined(); - expect(result.greeting).toBeDefined(); - expect(result.healthStatus).toBeDefined(); - expect(result.stats).toBeDefined(); - expect(result.reviewNeeded).toBeDefined(); - expect(result.peopleToReconnect).toBeDefined(); - expect(result.recentlyAdded).toBeDefined(); - }); - - it('should return correct time-based greeting', async () => { - const result = await tools.dailyBrief(); - const hour = new Date().getHours(); - - if (hour < 12) { - expect(result.greeting).toBe('Good morning'); - } else if (hour < 17) { - expect(result.greeting).toBe('Good afternoon'); - } else { - expect(result.greeting).toBe('Good evening'); - } - }); - - it('should include stats about the knowledge base', async () => { - const result = await tools.dailyBrief(); - - expect(result.stats.totalKnowledge).toBeGreaterThan(0); - expect(result.stats.peopleInNetwork).toBeGreaterThanOrEqual(0); - expect(result.stats.connections).toBeGreaterThanOrEqual(0); - expect(result.stats.databaseSize).toBeDefined(); - }); - - it('should return date in YYYY-MM-DD format', async () => { - const result = await tools.dailyBrief(); - - expect(result.date).toMatch(/^\d{4}-\d{2}-\d{2}$/); - }); - - it('should include recently added nodes', async () => { - const result = await tools.dailyBrief(); - - expect(result.recentlyAdded.length).toBeGreaterThan(0); - expect(result.recentlyAdded[0].id).toBeDefined(); - expect(result.recentlyAdded[0].preview).toBeDefined(); - }); - }); - - // ========================================================================== - // Tool 9: health_check - // ========================================================================== - describe('health_check tool', () => { - it('should return health status', async () => { - const result = await tools.healthCheck(); - - expect(result.status).toBeDefined(); - expect(['healthy', 'warning', 'critical']).toContain(result.status); - }); - - it('should include database size information', async () => { - const result = await tools.healthCheck(); - - expect(result.databaseSize).toBeDefined(); - expect(result.databaseSize.bytes).toBeGreaterThanOrEqual(0); - expect(result.databaseSize.mb).toBeGreaterThanOrEqual(0); - expect(result.databaseSize.formatted).toBeDefined(); - }); - - it('should include node and people counts', async () => { - const result = await tools.healthCheck(); - - expect(result.nodeCount).toBeGreaterThanOrEqual(0); - expect(result.peopleCount).toBeGreaterThanOrEqual(0); - expect(result.edgeCount).toBeGreaterThanOrEqual(0); - }); - - it('should check WAL mode status', async () => { - const result = await tools.healthCheck(); - - expect(typeof result.walMode).toBe('boolean'); - }); - - it('should perform integrity check', async () => { - const result = await tools.healthCheck(); - - expect(typeof result.integrityCheck).toBe('boolean'); - // In-memory databases should pass integrity check - expect(result.integrityCheck).toBe(true); - }); - - it('should provide recommendations', async () => { - const result = await tools.healthCheck(); - - expect(result.recommendations).toBeDefined(); - expect(Array.isArray(result.recommendations)).toBe(true); - expect(result.recommendations.length).toBeGreaterThan(0); - }); - - it('should include warnings array', async () => { - const result = await tools.healthCheck(); - - expect(result.warnings).toBeDefined(); - expect(Array.isArray(result.warnings)).toBe(true); - }); - }); - - // ========================================================================== - // Tool 10: backup - // ========================================================================== - describe('backup tool', () => { - // Note: Backup tests use the real filesystem, but with in-memory DB - // The backup will fail gracefully for :memory: databases - - it('should handle backup gracefully for in-memory databases', async () => { - // For in-memory databases, backup will return success: false - // This is expected behavior - const result = await tools.backup(); - - // Either succeeds or fails gracefully - expect(typeof result.success).toBe('boolean'); - expect(result.message).toBeDefined(); - expect(result.totalBackups).toBeGreaterThanOrEqual(0); - expect(Array.isArray(result.backups)).toBe(true); - }); - - it('should return backup metadata structure', async () => { - const result = await tools.backup(); - - // Check structure regardless of success/failure - expect(typeof result.totalBackups).toBe('number'); - expect(Array.isArray(result.backups)).toBe(true); - }); - }); - - // ========================================================================== - // Tool 11: list_backups - // ========================================================================== - describe('list_backups tool', () => { - it('should return backups list structure', async () => { - const result = await tools.listBackups(); - - expect(result.totalBackups).toBeGreaterThanOrEqual(0); - expect(Array.isArray(result.backups)).toBe(true); - }); - - it('should include backup metadata for each entry', async () => { - // First create a backup if possible - try { - await tools.backup(); - } catch { - // May fail for in-memory DB - } - - const result = await tools.listBackups(); - - if (result.backups.length > 0) { - const backup = result.backups[0]; - expect(backup.path).toBeDefined(); - expect(backup.size).toBeDefined(); - expect(backup.date).toBeDefined(); - } - }); - }); - - // ========================================================================== - // Tool 12: optimize_database - // ========================================================================== - describe('optimize_database tool', () => { - it('should successfully optimize database', async () => { - // Add and delete some data to create fragmentation - for (let i = 0; i < 10; i++) { - const result = await tools.ingest({ content: `Temporary node ${i}` }); - db.deleteNode(result.nodeId); - } - - const result = await tools.optimizeDatabase(); - - expect(result.success).toBe(true); - expect(result.message).toContain('optimized'); - }); - - it('should return size before and after', async () => { - const result = await tools.optimizeDatabase(); - - expect(result.sizeBefore).toBeDefined(); - expect(result.sizeAfter).toBeDefined(); - expect(result.spaceSaved).toBeDefined(); - }); - - it('should not crash on empty database', async () => { - const emptyDb = createTestDatabase(); - const emptyTools = createMCPToolHandler(emptyDb); - - const result = await emptyTools.optimizeDatabase(); - - expect(result.success).toBe(true); - emptyDb.close(); - }); - }); - - // ========================================================================== - // Tool 13: apply_decay - // ========================================================================== - describe('apply_decay tool', () => { - it('should apply decay and return count', async () => { - // Create nodes with old access dates - for (let i = 0; i < 5; i++) { - await tools.ingest({ content: `Decay test node ${i}` }); - } - - // Manually set old access dates - db['db'].prepare(` - UPDATE knowledge_nodes - SET last_accessed_at = datetime('now', '-30 days') - WHERE content LIKE 'Decay test node%' - `).run(); - - const result = await tools.applyDecay(); - - expect(result.success).toBe(true); - expect(result.nodesUpdated).toBeGreaterThanOrEqual(0); - expect(result.message).toContain('Applied decay'); - }); - - it('should decrease retention strength for old nodes', async () => { - const ingestResult = await tools.ingest({ - content: 'Old node for decay verification', - }); - - // Set access date to 30 days ago - db['db'] - .prepare( - `UPDATE knowledge_nodes SET last_accessed_at = datetime('now', '-30 days') WHERE id = ?` - ) - .run(ingestResult.nodeId); - - const nodeBefore = db.getNode(ingestResult.nodeId); - expect(nodeBefore?.retentionStrength).toBe(1.0); - - await tools.applyDecay(); - - const nodeAfter = db.getNode(ingestResult.nodeId); - expect(nodeAfter?.retentionStrength).toBeLessThan(1.0); - }); - - it('should not decay recently accessed nodes significantly', async () => { - const ingestResult = await tools.ingest({ - content: 'Fresh node for decay test', - }); - - const nodeBefore = db.getNode(ingestResult.nodeId); - expect(nodeBefore?.retentionStrength).toBe(1.0); - - await tools.applyDecay(); - - const nodeAfter = db.getNode(ingestResult.nodeId); - // Recently accessed nodes should retain most of their strength - // The dual-strength model uses storage_strength as a factor, so some decay is normal - // but it should still be above 0.6 for very recently created nodes - expect(nodeAfter?.retentionStrength).toBeGreaterThan(0.6); - }); - - it('should apply slower decay to emotional content', async () => { - // Create two nodes - one emotional, one neutral - const emotionalNode = await tools.ingest({ - content: 'I am absolutely THRILLED about this amazing breakthrough! This is incredible!', - }); - const neutralNode = await tools.ingest({ - content: 'The meeting is scheduled for Tuesday at 3pm in room 204.', - }); - - // Verify emotional content was detected - const emotionalBefore = db.getNode(emotionalNode.nodeId); - const neutralBefore = db.getNode(neutralNode.nodeId); - - // The sentiment intensity should be higher for emotional content - expect(emotionalBefore?.sentimentIntensity).toBeGreaterThanOrEqual(0); - expect(neutralBefore?.sentimentIntensity).toBeGreaterThanOrEqual(0); - - // Set both to 30 days old - db['db'] - .prepare( - `UPDATE knowledge_nodes SET last_accessed_at = datetime('now', '-30 days') WHERE id IN (?, ?)` - ) - .run(emotionalNode.nodeId, neutralNode.nodeId); - - await tools.applyDecay(); - - const emotionalAfter = db.getNode(emotionalNode.nodeId); - const neutralAfter = db.getNode(neutralNode.nodeId); - - // Both should have decayed - expect(emotionalAfter?.retentionStrength).toBeLessThan(1.0); - expect(neutralAfter?.retentionStrength).toBeLessThan(1.0); - - // Emotional content should decay slower (higher retention) - // or at least not decay faster - the difference may be small - expect(emotionalAfter?.retentionStrength).toBeGreaterThanOrEqual( - neutralAfter?.retentionStrength ?? 0 - ); - }); - }); - - // ========================================================================== - // Additional Edge Cases and Error Handling - // ========================================================================== - describe('edge cases and error handling', () => { - it('should handle very long content in ingest', async () => { - const longContent = 'A'.repeat(10000); - const result = await tools.ingest({ content: longContent }); - - expect(result.success).toBe(true); - - const node = db.getNode(result.nodeId); - expect(node?.content.length).toBe(10000); - }); - - it('should handle special characters in content', async () => { - const specialContent = 'Test with "quotes" and and &entities;'; - const result = await tools.ingest({ content: specialContent }); - - const node = db.getNode(result.nodeId); - expect(node?.content).toBe(specialContent); - }); - - it('should handle unicode content', async () => { - const unicodeContent = 'Test with emoji: 🎉 and Japanese: こんにちは and Arabic: مرحبا'; - const result = await tools.ingest({ content: unicodeContent }); - - const node = db.getNode(result.nodeId); - expect(node?.content).toBe(unicodeContent); - }); - - it('should handle empty tags array', async () => { - const result = await tools.ingest({ - content: 'Node with empty tags', - tags: [], - }); - - const node = db.getNode(result.nodeId); - expect(node?.tags).toHaveLength(0); - }); - - it('should handle concurrent operations', async () => { - // Simulate concurrent ingests - const promises = []; - for (let i = 0; i < 10; i++) { - promises.push(tools.ingest({ content: `Concurrent node ${i}` })); - } - - const results = await Promise.all(promises); - - expect(results).toHaveLength(10); - expect(results.every((r) => r.success)).toBe(true); - }); - - it('should handle rapid mark_reviewed calls', async () => { - const ingestResult = await tools.ingest({ - content: 'Node for rapid review test', - }); - - // Call mark_reviewed multiple times rapidly - const results = []; - for (let i = 0; i < 5; i++) { - results.push(await tools.markReviewed({ nodeId: ingestResult.nodeId })); - } - - // All should succeed - expect(results.every((r) => r.success)).toBe(true); - - // Review count should be 5 - const node = db.getNode(ingestResult.nodeId); - expect(node?.reviewCount).toBe(5); - }); - }); -}); diff --git a/packages/core/src/__tests__/setup.ts b/packages/core/src/__tests__/setup.ts deleted file mode 100644 index 821f7c5..0000000 --- a/packages/core/src/__tests__/setup.ts +++ /dev/null @@ -1,300 +0,0 @@ -import Database from 'better-sqlite3'; -import type { KnowledgeNodeInput, PersonNode, GraphEdge } from '../core/types.js'; - -/** - * Create an in-memory database for testing - */ -export function createTestDatabase(): Database.Database { - const db = new Database(':memory:'); - db.pragma('journal_mode = WAL'); - db.pragma('foreign_keys = ON'); - - // Initialize tables (from database.ts initializeSchema) - db.exec(` - CREATE TABLE IF NOT EXISTS knowledge_nodes ( - id TEXT PRIMARY KEY, - content TEXT NOT NULL, - summary TEXT, - - -- Temporal metadata - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - last_accessed_at TEXT NOT NULL, - access_count INTEGER DEFAULT 0, - - -- Decay modeling (SM-2 inspired spaced repetition) - retention_strength REAL DEFAULT 1.0, - stability_factor REAL DEFAULT 1.0, - sentiment_intensity REAL DEFAULT 0, - next_review_date TEXT, - review_count INTEGER DEFAULT 0, - - -- Dual-Strength Memory Model (Bjork & Bjork, 1992) - storage_strength REAL DEFAULT 1.0, - retrieval_strength REAL DEFAULT 1.0, - - -- Provenance - source_type TEXT NOT NULL, - source_platform TEXT NOT NULL, - source_id TEXT, - source_url TEXT, - source_chain TEXT DEFAULT '[]', - git_context TEXT, - - -- Confidence - confidence REAL DEFAULT 0.8, - is_contradicted INTEGER DEFAULT 0, - contradiction_ids TEXT DEFAULT '[]', - - -- Extracted entities (JSON arrays) - people TEXT DEFAULT '[]', - concepts TEXT DEFAULT '[]', - events TEXT DEFAULT '[]', - tags TEXT DEFAULT '[]' - ); - - CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON knowledge_nodes(created_at); - CREATE INDEX IF NOT EXISTS idx_nodes_last_accessed ON knowledge_nodes(last_accessed_at); - CREATE INDEX IF NOT EXISTS idx_nodes_retention ON knowledge_nodes(retention_strength); - CREATE INDEX IF NOT EXISTS idx_nodes_source_type ON knowledge_nodes(source_type); - CREATE INDEX IF NOT EXISTS idx_nodes_source_platform ON knowledge_nodes(source_platform); - `); - - // Full-text search for content - db.exec(` - CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5( - id, - content, - summary, - tags, - content='knowledge_nodes', - content_rowid='rowid' - ); - - -- Triggers to keep FTS in sync - CREATE TRIGGER IF NOT EXISTS knowledge_ai AFTER INSERT ON knowledge_nodes BEGIN - INSERT INTO knowledge_fts(rowid, id, content, summary, tags) - VALUES (NEW.rowid, NEW.id, NEW.content, NEW.summary, NEW.tags); - END; - - CREATE TRIGGER IF NOT EXISTS knowledge_ad AFTER DELETE ON knowledge_nodes BEGIN - INSERT INTO knowledge_fts(knowledge_fts, rowid, id, content, summary, tags) - VALUES ('delete', OLD.rowid, OLD.id, OLD.content, OLD.summary, OLD.tags); - END; - - CREATE TRIGGER IF NOT EXISTS knowledge_au AFTER UPDATE ON knowledge_nodes BEGIN - INSERT INTO knowledge_fts(knowledge_fts, rowid, id, content, summary, tags) - VALUES ('delete', OLD.rowid, OLD.id, OLD.content, OLD.summary, OLD.tags); - INSERT INTO knowledge_fts(rowid, id, content, summary, tags) - VALUES (NEW.rowid, NEW.id, NEW.content, NEW.summary, NEW.tags); - END; - `); - - // People table - db.exec(` - CREATE TABLE IF NOT EXISTS people ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - aliases TEXT DEFAULT '[]', - - -- Relationship context - how_we_met TEXT, - relationship_type TEXT, - organization TEXT, - role TEXT, - location TEXT, - - -- Contact info - email TEXT, - phone TEXT, - social_links TEXT DEFAULT '{}', - - -- Communication patterns - last_contact_at TEXT, - contact_frequency REAL DEFAULT 0, - preferred_channel TEXT, - - -- Shared context - shared_topics TEXT DEFAULT '[]', - shared_projects TEXT DEFAULT '[]', - - -- Meta - notes TEXT, - relationship_health REAL DEFAULT 0.5, - - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_people_name ON people(name); - CREATE INDEX IF NOT EXISTS idx_people_last_contact ON people(last_contact_at); - `); - - // Interactions table - db.exec(` - CREATE TABLE IF NOT EXISTS interactions ( - id TEXT PRIMARY KEY, - person_id TEXT NOT NULL, - type TEXT NOT NULL, - date TEXT NOT NULL, - summary TEXT NOT NULL, - topics TEXT DEFAULT '[]', - sentiment REAL, - action_items TEXT DEFAULT '[]', - source_node_id TEXT, - - FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE, - FOREIGN KEY (source_node_id) REFERENCES knowledge_nodes(id) ON DELETE SET NULL - ); - - CREATE INDEX IF NOT EXISTS idx_interactions_person ON interactions(person_id); - CREATE INDEX IF NOT EXISTS idx_interactions_date ON interactions(date); - `); - - // Graph edges table - db.exec(` - CREATE TABLE IF NOT EXISTS graph_edges ( - id TEXT PRIMARY KEY, - from_id TEXT NOT NULL, - to_id TEXT NOT NULL, - edge_type TEXT NOT NULL, - weight REAL DEFAULT 0.5, - metadata TEXT DEFAULT '{}', - created_at TEXT NOT NULL, - - UNIQUE(from_id, to_id, edge_type) - ); - - CREATE INDEX IF NOT EXISTS idx_edges_from ON graph_edges(from_id); - CREATE INDEX IF NOT EXISTS idx_edges_to ON graph_edges(to_id); - CREATE INDEX IF NOT EXISTS idx_edges_type ON graph_edges(edge_type); - `); - - // Sources table - db.exec(` - CREATE TABLE IF NOT EXISTS sources ( - id TEXT PRIMARY KEY, - type TEXT NOT NULL, - platform TEXT NOT NULL, - original_id TEXT, - url TEXT, - file_path TEXT, - title TEXT, - author TEXT, - publication_date TEXT, - - ingested_at TEXT NOT NULL, - last_synced_at TEXT NOT NULL, - content_hash TEXT, - - node_count INTEGER DEFAULT 0 - ); - - CREATE INDEX IF NOT EXISTS idx_sources_platform ON sources(platform); - CREATE INDEX IF NOT EXISTS idx_sources_file_path ON sources(file_path); - `); - - // Embeddings reference table - db.exec(` - CREATE TABLE IF NOT EXISTS embeddings ( - node_id TEXT PRIMARY KEY, - chroma_id TEXT NOT NULL, - model TEXT NOT NULL, - created_at TEXT NOT NULL, - - FOREIGN KEY (node_id) REFERENCES knowledge_nodes(id) ON DELETE CASCADE - ); - `); - - // Metadata table - db.exec(` - CREATE TABLE IF NOT EXISTS vestige_metadata ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - `); - - return db; -} - -/** - * Create test fixtures for knowledge nodes - */ -export function createTestNode(overrides: Partial> = {}): Omit { - return { - content: 'Test content for knowledge node', - sourceType: 'manual', - sourcePlatform: 'manual', - tags: [], - people: [], - concepts: [], - events: [], - ...overrides, - }; -} - -/** - * Create test fixtures for people - */ -export function createTestPerson(overrides: Partial> = {}): Omit { - return { - name: 'Test Person', - relationshipType: 'colleague', - aliases: [], - socialLinks: {}, - contactFrequency: 0, - sharedTopics: [], - sharedProjects: [], - relationshipHealth: 0.5, - ...overrides, - }; -} - -/** - * Create test fixtures for graph edges - */ -export function createTestEdge(fromId: string, toId: string, overrides: Partial> = {}): Omit { - return { - fromId, - toId, - edgeType: 'relates_to', - weight: 0.5, - metadata: {}, - ...overrides, - }; -} - -/** - * Clean up test database - */ -export function cleanupTestDatabase(db: Database.Database): void { - try { - db.close(); - } catch { - // Ignore close errors - } -} - -/** - * Wait for a specified amount of time (useful for async tests) - */ -export function wait(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -/** - * Generate a unique test ID - */ -export function generateTestId(): string { - return `test-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; -} - -/** - * Create a mock timestamp for consistent testing - */ -export function mockTimestamp(daysAgo: number = 0): Date { - const date = new Date(); - date.setDate(date.getDate() - daysAgo); - return date; -} diff --git a/packages/core/src/cli.ts b/packages/core/src/cli.ts deleted file mode 100644 index a4a40d5..0000000 --- a/packages/core/src/cli.ts +++ /dev/null @@ -1,1464 +0,0 @@ -#!/usr/bin/env node - -/** - * Vestige CLI - Management commands for the Memory Palace - * - * Usage: - * vestige stats - Show knowledge base statistics and health - * vestige health - Detailed health check - * vestige review - Start a review session - * vestige people - List people in your network - * vestige backup - Create a backup - * vestige backups - List available backups - * vestige restore - Restore from a backup - * vestige optimize - Optimize the database - * vestige decay - Apply memory decay - * vestige eat - Ingest documentation/content (Man Page Absorber) - */ - -import { VestigeDatabase, VestigeDatabaseError } from './core/database.js'; -import { - captureContext, - formatContextForInjection, - startContextWatcher, - readSavedContext, -} from './core/context-watcher.js'; -import { runREMCycle, previewREMCycle } from './core/rem-cycle.js'; -import { ShadowSelf, runShadowCycle } from './core/shadow-self.js'; -import { - validatePath, - validateUrl, - sanitizeContent, - logSecurityEvent, - MAX_CONTENT_LENGTH, -} from './core/security.js'; -import { runConsolidation } from './core/consolidation.js'; -import { createEmbeddingService, OllamaEmbeddingService } from './core/embeddings.js'; -import { getConfig, resetConfig, loadConfig } from './core/config.js'; -import { createVectorStore, ChromaVectorStore } from './core/vector-store.js'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { marked } from 'marked'; - -// ============================================================================ -// MAN PAGE ABSORBER - Feed your brain -// ============================================================================ - -interface ContentChunk { - title: string; - content: string; - section: string; - index: number; -} - -/** - * Fetch content from URL (with SSRF protection) - */ -async function fetchUrl(url: string): Promise { - // Validate URL to prevent SSRF attacks - const validation = validateUrl(url); - if (!validation.valid) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { error: validation.error || 'URL validation failed', url: url.slice(0, 100) }, - severity: 'high', - blocked: true, - }); - throw new Error(`Security: ${validation.error}`); - } - - const safeUrl = validation.sanitizedUrl!; - const response = await fetch(safeUrl, { - // Add timeout to prevent hanging on slow responses - signal: AbortSignal.timeout(30000), // 30 second timeout - }); - - if (!response.ok) { - throw new Error(`Failed to fetch ${safeUrl}: ${response.status} ${response.statusText}`); - } - - // Check content length to prevent DoS - const contentLength = response.headers.get('content-length'); - if (contentLength && parseInt(contentLength, 10) > MAX_CONTENT_LENGTH) { - throw new Error(`Content too large: ${contentLength} bytes exceeds ${MAX_CONTENT_LENGTH} byte limit`); - } - - const contentType = response.headers.get('content-type') || ''; - - let content: string; - if (contentType.includes('text/html')) { - // Strip HTML to get text content - const html = await response.text(); - content = stripHtml(html); - } else { - content = await response.text(); - } - - // Sanitize the content - return sanitizeContent(content); -} - -/** - * Simple HTML stripper - extracts text content - */ -function stripHtml(html: string): string { - // Remove script and style tags and their content - let text = html.replace(/]*>[\s\S]*?<\/script>/gi, ''); - text = text.replace(/]*>[\s\S]*?<\/style>/gi, ''); - - // Remove HTML tags - text = text.replace(/<[^>]+>/g, ' '); - - // Decode HTML entities - text = text.replace(/ /g, ' '); - text = text.replace(/&/g, '&'); - text = text.replace(/</g, '<'); - text = text.replace(/>/g, '>'); - text = text.replace(/"/g, '"'); - text = text.replace(/'/g, "'"); - - // Clean up whitespace - text = text.replace(/\s+/g, ' ').trim(); - - return text; -} - -/** - * Read content from file (with path traversal protection) - */ -async function readFile(filePath: string): Promise { - // Validate path to prevent path traversal attacks - const validation = validatePath(filePath); - if (!validation.valid) { - logSecurityEvent({ - type: 'path_traversal', - details: { error: validation.error || 'Path validation failed', path: filePath.slice(0, 100) }, - severity: 'high', - blocked: true, - }); - throw new Error(`Security: ${validation.error}`); - } - - const safePath = validation.sanitizedPath!; - - if (!fs.existsSync(safePath)) { - throw new Error(`File not found: ${safePath}`); - } - - // Check file size before reading - const stats = fs.statSync(safePath); - if (stats.size > MAX_CONTENT_LENGTH) { - throw new Error(`File too large: ${stats.size} bytes exceeds ${MAX_CONTENT_LENGTH} byte limit`); - } - - const content = fs.readFileSync(safePath, 'utf-8'); - - // Sanitize the content - return sanitizeContent(content); -} - -/** - * Chunk content intelligently - * - Respects markdown headers as section boundaries - * - Creates overlapping chunks for context preservation - * - Targets ~500-1000 tokens per chunk - */ -function chunkContent(content: string, source: string): ContentChunk[] { - const chunks: ContentChunk[] = []; - - // Try to detect if it's markdown - const isMarkdown = content.includes('# ') || content.includes('## ') || content.includes('```'); - - if (isMarkdown) { - // Split by headers - const sections = content.split(/^(#{1,3} .+)$/m); - let currentSection = 'Introduction'; - let currentContent = ''; - let chunkIndex = 0; - - for (let i = 0; i < sections.length; i++) { - const sectionRaw = sections[i]; - if (!sectionRaw) continue; - const section = sectionRaw.trim(); - if (!section) continue; - - // Check if this is a header - if (section.match(/^#{1,3} /)) { - // Save previous section if it has content - if (currentContent.trim()) { - chunks.push(...splitLargeSection(currentContent, currentSection, chunkIndex, source)); - chunkIndex = chunks.length; - } - currentSection = section.replace(/^#{1,3} /, '').trim(); - currentContent = ''; - } else { - currentContent += section + '\n\n'; - } - } - - // Don't forget the last section - if (currentContent.trim()) { - chunks.push(...splitLargeSection(currentContent, currentSection, chunkIndex, source)); - } - } else { - // Plain text - split by paragraphs - const paragraphs = content.split(/\n\n+/); - let currentChunk = ''; - let chunkIndex = 0; - - for (const para of paragraphs) { - const trimmed = para.trim(); - if (!trimmed) continue; - - if ((currentChunk + trimmed).length > 2000) { - if (currentChunk) { - chunks.push({ - title: `Section ${chunkIndex + 1}`, - content: currentChunk.trim(), - section: source, - index: chunkIndex, - }); - chunkIndex++; - } - currentChunk = trimmed + '\n\n'; - } else { - currentChunk += trimmed + '\n\n'; - } - } - - if (currentChunk.trim()) { - chunks.push({ - title: `Section ${chunkIndex + 1}`, - content: currentChunk.trim(), - section: source, - index: chunkIndex, - }); - } - } - - return chunks; -} - -/** - * Split large sections into smaller chunks - */ -function splitLargeSection(content: string, section: string, startIndex: number, source: string): ContentChunk[] { - const MAX_CHUNK_SIZE = 2000; // ~500 tokens - const chunks: ContentChunk[] = []; - - if (content.length <= MAX_CHUNK_SIZE) { - chunks.push({ - title: section, - content: content.trim(), - section: source, - index: startIndex, - }); - return chunks; - } - - // Split by paragraphs - const paragraphs = content.split(/\n\n+/); - let currentChunk = ''; - let partNumber = 1; - - for (const para of paragraphs) { - const trimmed = para.trim(); - if (!trimmed) continue; - - if ((currentChunk + trimmed).length > MAX_CHUNK_SIZE) { - if (currentChunk) { - chunks.push({ - title: `${section} (Part ${partNumber})`, - content: currentChunk.trim(), - section: source, - index: startIndex + chunks.length, - }); - partNumber++; - } - currentChunk = trimmed + '\n\n'; - } else { - currentChunk += trimmed + '\n\n'; - } - } - - if (currentChunk.trim()) { - chunks.push({ - title: partNumber > 1 ? `${section} (Part ${partNumber})` : section, - content: currentChunk.trim(), - section: source, - index: startIndex + chunks.length, - }); - } - - return chunks; -} - -/** - * Ingest content from URL or file path - */ -async function eatContent(source: string, db: VestigeDatabase): Promise { - console.log(`\n Fetching content from: ${source}`); - - // Determine if URL or file - const isUrl = source.startsWith('http://') || source.startsWith('https://'); - let content: string; - let sourceType: 'webpage' | 'article' = 'webpage'; - let sourceName: string; - - if (isUrl) { - content = await fetchUrl(source); - sourceName = new URL(source).hostname + new URL(source).pathname; - } else { - content = await readFile(source); - sourceName = path.basename(source); - sourceType = 'article'; // Local files are treated as articles - } - - console.log(` Content length: ${content.length} characters`); - - // Chunk the content - const chunks = chunkContent(content, sourceName); - console.log(` Created ${chunks.length} knowledge chunks`); - - if (chunks.length === 0) { - console.log(' No content to ingest.\n'); - return; - } - - // Ingest each chunk - console.log('\n Ingesting chunks:'); - const nodeIds: string[] = []; - - for (const chunk of chunks) { - const node = db.insertNode({ - content: chunk.content, - summary: chunk.title, - sourceType: sourceType, - sourcePlatform: isUrl ? 'browser' : 'manual', - sourceUrl: isUrl ? source : undefined, - createdAt: new Date(), - updatedAt: new Date(), - lastAccessedAt: new Date(), - accessCount: 0, - retentionStrength: 1.0, - stabilityFactor: 1.0, - reviewCount: 0, - confidence: 0.9, // Ingested docs are high confidence - isContradicted: false, - contradictionIds: [], - people: [], - concepts: [], - events: [], - tags: ['ingested', chunk.section.toLowerCase().replace(/[^a-z0-9]+/g, '-')], - sourceChain: [source], - }); - - nodeIds.push(node.id); - console.log(` [${node.id.slice(0, 8)}] ${chunk.title.slice(0, 50)}${chunk.title.length > 50 ? '...' : ''}`); - } - - // Create edges between sequential chunks (they're related!) - console.log('\n Creating knowledge connections...'); - let edgesCreated = 0; - for (let i = 0; i < nodeIds.length - 1; i++) { - const fromId = nodeIds[i]; - const toId = nodeIds[i + 1]; - if (!fromId || !toId) continue; - - try { - db.insertEdge({ - fromId, - toId, - edgeType: 'follows', - weight: 0.8, - metadata: { source: 'ingestion', order: i }, - createdAt: new Date(), - }); - edgesCreated++; - } catch { - // Edge might already exist - } - } - - console.log(` Created ${edgesCreated} sequential connections`); - console.log(`\n Successfully ingested ${chunks.length} chunks from ${sourceName}`); - console.log(` Use 'vestige recall' or ask Claude to find this knowledge.\n`); -} - -const command = process.argv[2]; -const args = process.argv.slice(3); - -async function main() { - const db = new VestigeDatabase(); - - try { - switch (command) { - case 'stats': { - const detailed = args[0] === 'detailed'; - const stats = db.getStats(); - const health = db.checkHealth(); - const size = db.getDatabaseSize(); - - console.log('\n Memory Statistics'); - console.log(' -----------------'); - console.log(` Status: ${getStatusEmoji(health.status)} ${health.status.toUpperCase()}`); - console.log(` Total nodes: ${stats.totalNodes}`); - console.log(` Total people: ${stats.totalPeople}`); - console.log(` Total connections: ${stats.totalEdges}`); - console.log(` Database Size: ${size.formatted}`); - console.log(` Last Backup: ${health.lastBackup || 'Never'}`); - - if (health.warnings.length > 0) { - console.log('\n Warnings:'); - for (const warning of health.warnings) { - console.log(` - ${warning}`); - } - } - - const decaying = db.getDecayingNodes(0.5, { limit: 100 }); - console.log(`\n Knowledge needing review: ${decaying.total} nodes`); - - if (detailed) { - // Retention strength distribution - console.log('\n Retention Strength Distribution'); - console.log(' --------------------------------'); - - const allNodes = db.getRecentNodes({ limit: 10000 }); - const distribution = { - strong: 0, // 0.8-1.0 - good: 0, // 0.6-0.8 - moderate: 0, // 0.4-0.6 - weak: 0, // 0.2-0.4 - fading: 0, // 0.0-0.2 - }; - - for (const node of allNodes.items) { - const strength = node.retentionStrength; - if (strength >= 0.8) distribution.strong++; - else if (strength >= 0.6) distribution.good++; - else if (strength >= 0.4) distribution.moderate++; - else if (strength >= 0.2) distribution.weak++; - else distribution.fading++; - } - - const total = allNodes.items.length || 1; - console.log(` Strong (80-100%): ${distribution.strong} (${((distribution.strong / total) * 100).toFixed(1)}%)`); - console.log(` Good (60-80%): ${distribution.good} (${((distribution.good / total) * 100).toFixed(1)}%)`); - console.log(` Moderate (40-60%): ${distribution.moderate} (${((distribution.moderate / total) * 100).toFixed(1)}%)`); - console.log(` Weak (20-40%): ${distribution.weak} (${((distribution.weak / total) * 100).toFixed(1)}%)`); - console.log(` Fading (0-20%): ${distribution.fading} (${((distribution.fading / total) * 100).toFixed(1)}%)`); - - // Source type breakdown - console.log('\n Source Type Breakdown'); - console.log(' ---------------------'); - - const sourceTypes: Record = {}; - for (const node of allNodes.items) { - const type = node.sourceType || 'unknown'; - sourceTypes[type] = (sourceTypes[type] || 0) + 1; - } - - const sortedTypes = Object.entries(sourceTypes).sort((a, b) => b[1] - a[1]); - for (const [type, count] of sortedTypes.slice(0, 10)) { - console.log(` ${type.padEnd(15)} ${count}`); - } - - // Services status - console.log('\n Services Status'); - console.log(' ---------------'); - - try { - const embService = new OllamaEmbeddingService(); - const embAvailable = await embService.isAvailable(); - console.log(` Embeddings: ${embAvailable ? 'Available (Ollama)' : 'Fallback mode'}`); - } catch { - console.log(' Embeddings: Check failed'); - } - - try { - const chromaStore = new ChromaVectorStore(); - const vecAvailable = await chromaStore.isAvailable(); - if (vecAvailable) { - const vecStats = await chromaStore.getStats(); - console.log(` Vector Store: ChromaDB (${vecStats.embeddingCount} embeddings)`); - } else { - console.log(' Vector Store: SQLite fallback'); - } - await chromaStore.close(); - } catch { - console.log(' Vector Store: Check failed'); - } - - // FSRS config - const config = getConfig(); - console.log(` FSRS Retention: ${(config.fsrs.desiredRetention * 100).toFixed(0)}%`); - } - - console.log(); - break; - } - - case 'health': { - const health = db.checkHealth(); - const size = db.getDatabaseSize(); - - console.log('\n Vestige Health Check\n'); - console.log(` Status: ${getStatusEmoji(health.status)} ${health.status.toUpperCase()}`); - console.log(` Database Path: ${health.dbPath}`); - console.log(` Database Size: ${size.formatted}`); - console.log(` WAL Mode: ${health.walMode ? 'Enabled' : 'Disabled'}`); - console.log(` Integrity Check: ${health.integrityCheck ? 'Passed' : 'FAILED'}`); - console.log(` Node Count: ${health.nodeCount}`); - console.log(` People Count: ${health.peopleCount}`); - console.log(` Edge Count: ${health.edgeCount}`); - console.log(` Last Backup: ${health.lastBackup || 'Never'}`); - - if (health.warnings.length > 0) { - console.log('\n Warnings:'); - for (const warning of health.warnings) { - console.log(` - ${warning}`); - } - } else { - console.log('\n No warnings - everything looks good!'); - } - console.log(); - break; - } - - case 'review': { - const decaying = db.getDecayingNodes(0.5, { limit: 10 }); - if (decaying.items.length === 0) { - console.log('\n No knowledge needs review right now!\n'); - break; - } - - console.log('\n Knowledge Due for Review\n'); - console.log(` Showing ${decaying.items.length} of ${decaying.total} items\n`); - - for (const node of decaying.items) { - console.log(` [${node.id.slice(0, 8)}] ${node.content.slice(0, 80)}...`); - console.log(` Retention: ${(node.retentionStrength * 100).toFixed(1)}%`); - const daysSince = Math.floor((Date.now() - node.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24)); - console.log(` Last accessed: ${daysSince} days ago`); - console.log(); - } - - if (decaying.hasMore) { - console.log(` ... and ${decaying.total - decaying.items.length} more items need review\n`); - } - break; - } - - case 'people': { - const result = db.getAllPeople({ limit: 50 }); - if (result.items.length === 0) { - console.log('\n No people in your network yet.\n'); - break; - } - - console.log('\n Your Network\n'); - console.log(` Showing ${result.items.length} of ${result.total} people\n`); - - for (const person of result.items) { - const daysSince = person.lastContactAt - ? Math.floor((Date.now() - person.lastContactAt.getTime()) / (1000 * 60 * 60 * 24)) - : null; - console.log(` ${person.name}`); - if (person.organization) console.log(` Organization: ${person.organization}`); - if (person.relationshipType) console.log(` Relationship: ${person.relationshipType}`); - if (daysSince !== null) console.log(` Last contact: ${daysSince} days ago`); - if (person.sharedTopics.length > 0) console.log(` Topics: ${person.sharedTopics.join(', ')}`); - console.log(); - } - break; - } - - case 'backup': { - console.log('\n Creating backup...'); - const backupPath = db.backup(); - console.log(` Backup created: ${backupPath}`); - - const backups = db.listBackups(); - console.log(`\n Total backups: ${backups.length}`); - console.log(' Recent backups:'); - for (const backup of backups.slice(0, 3)) { - console.log(` - ${backup.path}`); - console.log(` Size: ${(backup.size / 1024 / 1024).toFixed(2)}MB`); - console.log(` Date: ${backup.date.toISOString()}`); - } - console.log(); - break; - } - - case 'backups': { - const backups = db.listBackups(); - if (backups.length === 0) { - console.log('\n No backups found. Create one with: vestige backup\n'); - break; - } - - console.log('\n Available Backups\n'); - for (const backup of backups) { - console.log(` ${backup.path}`); - console.log(` Size: ${(backup.size / 1024 / 1024).toFixed(2)}MB`); - console.log(` Date: ${backup.date.toISOString()}`); - console.log(); - } - break; - } - - case 'restore': { - const backupPath = args[0]; - if (!backupPath) { - console.log('\n Usage: vestige restore '); - console.log(' Use "vestige backups" to see available backups.\n'); - break; - } - - // Validate path to prevent path traversal attacks - const pathValidation = validatePath(backupPath); - if (!pathValidation.valid) { - logSecurityEvent({ - type: 'path_traversal', - details: { error: pathValidation.error || 'Path validation failed', path: backupPath.slice(0, 100) }, - severity: 'high', - blocked: true, - }); - console.error(`\n Security Error: ${pathValidation.error}\n`); - break; - } - - const safePath = pathValidation.sanitizedPath!; - console.log(`\n Restoring from: ${safePath}`); - console.log(' WARNING: This will replace your current database!\n'); - - // In a real CLI, you'd prompt for confirmation here - // For now, we just do it - try { - db.restore(safePath); - console.log(' Restore completed successfully!\n'); - } catch (error) { - if (error instanceof VestigeDatabaseError) { - console.error(` Error: ${error.message} (${error.code})\n`); - } else { - console.error(` Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`); - } - } - break; - } - - case 'optimize': { - console.log('\n Optimizing database...'); - const sizeBefore = db.getDatabaseSize(); - db.optimize(); - const sizeAfter = db.getDatabaseSize(); - - console.log(` Size before: ${sizeBefore.formatted}`); - console.log(` Size after: ${sizeAfter.formatted}`); - console.log(` Space saved: ${(sizeBefore.mb - sizeAfter.mb).toFixed(2)}MB`); - console.log(); - break; - } - - case 'decay': { - console.log('\n Applying memory decay...'); - const updated = db.applyDecay(); - console.log(` Updated ${updated} knowledge nodes\n`); - break; - } - - case 'consolidate': - case 'sleep': { - console.log('\n Running sleep consolidation cycle...\n'); - const consResult = await runConsolidation(db); - console.log(` Short-term processed: ${consResult.shortTermProcessed}`); - console.log(` Promoted to long-term: ${consResult.promotedToLongTerm}`); - console.log(` Connections discovered: ${consResult.connectionsDiscovered}`); - console.log(` Edges pruned: ${consResult.edgesPruned}`); - console.log(` Decay applied: ${consResult.decayApplied}`); - console.log(`\n Duration: ${consResult.duration}ms\n`); - break; - } - - case 'embeddings': { - const embCmd = args[0]; - - switch (embCmd) { - case 'status': { - console.log('\n Embedding Service Status\n'); - const embService = new OllamaEmbeddingService(); - const available = await embService.isAvailable(); - console.log(` Service: ${available ? 'Available' : 'Not available'}`); - if (available) { - console.log(` Provider: Ollama`); - console.log(` Model: ${embService.getModel()}`); - console.log(` Host: ${process.env['OLLAMA_HOST'] || 'http://localhost:11434'}`); - } else { - console.log('\n To enable embeddings:'); - console.log(' 1. Install Ollama: https://ollama.ai'); - console.log(' 2. Run: ollama pull nomic-embed-text'); - console.log(' 3. Start Ollama service'); - } - console.log(); - break; - } - - case 'generate': { - const nodeId = args[1]; - console.log('\n Generating Embeddings\n'); - - try { - const embService = await createEmbeddingService(); - - if (nodeId) { - // Generate for specific node - const node = db.getNode(nodeId); - if (!node) { - console.log(` Error: Node not found: ${nodeId}\n`); - break; - } - - console.log(` Generating embedding for node: ${nodeId.slice(0, 8)}...`); - const embedding = await embService.generateEmbedding(node.content); - console.log(` Embedding generated: ${embedding.length} dimensions`); - console.log(` First 5 values: [${embedding.slice(0, 5).map(v => v.toFixed(4)).join(', ')}...]`); - } else { - // Generate for all nodes without embeddings - console.log(' Generating embeddings for all nodes...'); - const allNodes = db.getRecentNodes({ limit: 1000 }); - let generated = 0; - let failed = 0; - - for (const node of allNodes.items) { - try { - await embService.generateEmbedding(node.content); - generated++; - if (generated % 10 === 0) { - process.stdout.write(`\r Progress: ${generated}/${allNodes.items.length}`); - } - } catch { - failed++; - } - } - console.log(`\n Generated: ${generated}, Failed: ${failed}`); - } - } catch (error) { - console.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - console.log(); - break; - } - - case 'search': { - const query = args.slice(1).join(' '); - if (!query) { - console.log('\n Usage: vestige embeddings search ""\n'); - break; - } - - console.log(`\n Semantic Search: "${query}"\n`); - - try { - const embService = await createEmbeddingService(); - const queryEmbedding = await embService.generateEmbedding(query); - - // Get all nodes and compute similarity - const allNodes = db.getRecentNodes({ limit: 500 }); - const results: Array<{ node: typeof allNodes.items[0]; similarity: number }> = []; - - for (const node of allNodes.items) { - try { - const nodeEmbedding = await embService.generateEmbedding(node.content); - const similarity = embService.getSimilarity(queryEmbedding, nodeEmbedding); - results.push({ node, similarity }); - } catch { - // Skip nodes that fail to embed - } - } - - // Sort by similarity and show top 10 - results.sort((a, b) => b.similarity - a.similarity); - const topResults = results.slice(0, 10); - - if (topResults.length === 0) { - console.log(' No results found.\n'); - break; - } - - console.log(' Top Results:'); - for (const { node, similarity } of topResults) { - const preview = node.content.slice(0, 60).replace(/\n/g, ' '); - console.log(` [${(similarity * 100).toFixed(1)}%] ${preview}...`); - } - } catch (error) { - console.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - console.log(); - break; - } - - default: - console.log(` - Vestige Embeddings - Semantic Understanding - - Usage: vestige embeddings - - Commands: - status Check embedding service availability - generate [nodeId] Generate embeddings (all nodes or specific) - search "" Semantic similarity search - - Examples: - vestige embeddings status - vestige embeddings generate - vestige embeddings generate abc12345 - vestige embeddings search "authentication flow" -`); - } - break; - } - - case 'config': { - const configCmd = args[0]; - const configPath = path.join(os.homedir(), '.vestige', 'config.json'); - - switch (configCmd) { - case 'show': { - console.log('\n Vestige Configuration\n'); - const config = getConfig(); - console.log(JSON.stringify(config, null, 2)); - console.log(`\n Config file: ${configPath}\n`); - break; - } - - case 'set': { - const key = args[1]; - const value = args.slice(2).join(' '); - - if (!key || !value) { - console.log('\n Usage: vestige config set '); - console.log('\n Examples:'); - console.log(' vestige config set logging.level debug'); - console.log(' vestige config set fsrs.desiredRetention 0.85'); - console.log(' vestige config set rem.enabled false\n'); - break; - } - - console.log(`\n Setting ${key} = ${value}\n`); - - // Load existing config or create empty - let fileConfig: Record = {}; - if (fs.existsSync(configPath)) { - try { - fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - } catch { - console.log(' Warning: Could not parse existing config, starting fresh'); - } - } - - // Parse the key path (e.g., "logging.level") - const keyParts = key.split('.'); - let current: Record = fileConfig; - - for (let i = 0; i < keyParts.length - 1; i++) { - const part = keyParts[i]!; - if (!(part in current) || typeof current[part] !== 'object') { - current[part] = {}; - } - current = current[part] as Record; - } - - // Parse value (try as JSON, fall back to string) - let parsedValue: unknown = value; - try { - parsedValue = JSON.parse(value); - } catch { - // Keep as string - } - - current[keyParts[keyParts.length - 1]!] = parsedValue; - - // Ensure directory exists - const configDir = path.dirname(configPath); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - - // Write config - fs.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2)); - - // Reset singleton to reload - resetConfig(); - - console.log(` Configuration updated: ${key} = ${JSON.stringify(parsedValue)}`); - console.log(` Saved to: ${configPath}\n`); - break; - } - - case 'reset': { - console.log('\n Resetting configuration to defaults...\n'); - - if (fs.existsSync(configPath)) { - // Create backup before deleting - const backupPath = `${configPath}.backup.${Date.now()}`; - fs.copyFileSync(configPath, backupPath); - console.log(` Backup created: ${backupPath}`); - - fs.unlinkSync(configPath); - console.log(` Removed: ${configPath}`); - } - - resetConfig(); - console.log(' Configuration reset to defaults.\n'); - break; - } - - default: - console.log(` - Vestige Configuration Management - - Usage: vestige config - - Commands: - show Display current configuration - set Update a configuration value - reset Reset to default configuration - - Examples: - vestige config show - vestige config set logging.level debug - vestige config set fsrs.desiredRetention 0.85 - vestige config reset - - Configuration Sections: - database - Database paths and settings - fsrs - Spaced repetition algorithm - memory - Dual-strength memory model - rem - REM cycle settings - consolidation - Sleep consolidation - embeddings - Embedding service - vectorStore - Vector database - logging - Log levels - limits - Size limits -`); - } - break; - } - - case 'test': { - console.log('\n Vestige Self-Test Suite\n'); - console.log(' Running diagnostic tests...\n'); - - let allPassed = true; - - // Test 1: Database - try { - const stats = db.getStats(); - console.log(` [PASS] Database: ${stats.totalNodes} nodes, ${stats.totalPeople} people, ${stats.totalEdges} edges`); - } catch (error) { - console.log(` [FAIL] Database: ${error instanceof Error ? error.message : 'Unknown error'}`); - allPassed = false; - } - - // Test 2: Embeddings - try { - const embService = new OllamaEmbeddingService(); - const embAvailable = await embService.isAvailable(); - if (embAvailable) { - console.log(` [PASS] Embeddings: Ollama available (${embService.getModel()})`); - } else { - console.log(' [WARN] Embeddings: Ollama not available (fallback will be used)'); - } - } catch (error) { - console.log(` [WARN] Embeddings: ${error instanceof Error ? error.message : 'Check failed'}`); - } - - // Test 3: Vector Store - try { - const chromaStore = new ChromaVectorStore(); - const vecAvailable = await chromaStore.isAvailable(); - if (vecAvailable) { - const vecStats = await chromaStore.getStats(); - console.log(` [PASS] Vector Store: ChromaDB available (${vecStats.embeddingCount} embeddings)`); - } else { - console.log(' [WARN] Vector Store: ChromaDB not available (SQLite fallback will be used)'); - } - await chromaStore.close(); - } catch (error) { - console.log(` [WARN] Vector Store: ${error instanceof Error ? error.message : 'Check failed'}`); - } - - // Test 4: Configuration - try { - const config = getConfig(); - console.log(` [PASS] Configuration: Loaded (FSRS retention: ${config.fsrs.desiredRetention})`); - } catch (error) { - console.log(` [FAIL] Configuration: ${error instanceof Error ? error.message : 'Load failed'}`); - allPassed = false; - } - - // Test 5: Database health - try { - const health = db.checkHealth(); - if (health.status === 'healthy') { - console.log(` [PASS] Health Check: ${health.status}`); - } else if (health.status === 'warning') { - console.log(` [WARN] Health Check: ${health.warnings.length} warning(s)`); - } else { - console.log(` [FAIL] Health Check: ${health.status}`); - allPassed = false; - } - } catch (error) { - console.log(` [FAIL] Health Check: ${error instanceof Error ? error.message : 'Check failed'}`); - allPassed = false; - } - - console.log(); - if (allPassed) { - console.log(' All core tests passed!\n'); - } else { - console.log(' Some tests failed. Review the output above.\n'); - } - break; - } - - case 'ingest': { - const content = args.join(' '); - if (!content) { - console.log('\n Usage: vestige ingest ""'); - console.log('\n Store knowledge directly into Vestige.'); - console.log('\n Examples:'); - console.log(' vestige ingest "API rate limit is 100 req/min"'); - console.log(' vestige ingest "Meeting with John: discussed Q4 roadmap"\n'); - break; - } - - console.log('\n Ingesting knowledge...\n'); - - try { - const node = db.insertNode({ - content: content, - summary: content.slice(0, 100), - sourceType: 'note', - sourcePlatform: 'manual', - createdAt: new Date(), - updatedAt: new Date(), - lastAccessedAt: new Date(), - accessCount: 0, - retentionStrength: 1.0, - stabilityFactor: 1.0, - reviewCount: 0, - confidence: 0.9, - isContradicted: false, - contradictionIds: [], - people: [], - concepts: [], - events: [], - tags: ['cli-ingested'], - sourceChain: ['cli'], - }); - - console.log(` Stored as node: ${node.id}`); - console.log(` Content: "${content.slice(0, 60)}${content.length > 60 ? '...' : ''}"`); - console.log('\n Knowledge successfully ingested!\n'); - } catch (error) { - console.error(` Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`); - } - break; - } - - case 'recall': { - const query = args.join(' '); - if (!query) { - console.log('\n Usage: vestige recall ""'); - console.log('\n Search your memories.'); - console.log('\n Examples:'); - console.log(' vestige recall "rate limit"'); - console.log(' vestige recall "meeting John"\n'); - break; - } - - console.log(`\n Searching memories for: "${query}"\n`); - - try { - const result = db.searchNodes(query, { limit: 10 }); - - if (result.items.length === 0) { - console.log(' No memories found matching your query.\n'); - break; - } - - console.log(` Found ${result.total} memories (showing ${result.items.length}):\n`); - - for (const node of result.items) { - const preview = node.content.slice(0, 80).replace(/\n/g, ' '); - const daysSince = Math.floor((Date.now() - node.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24)); - console.log(` [${node.id.slice(0, 8)}] ${preview}${node.content.length > 80 ? '...' : ''}`); - console.log(` Retention: ${(node.retentionStrength * 100).toFixed(1)}% | Last accessed: ${daysSince}d ago`); - console.log(); - } - } catch (error) { - console.error(` Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`); - } - break; - } - - case 'eat': { - const source = args[0]; - if (!source) { - console.log('\n Usage: vestige eat '); - console.log('\n Examples:'); - console.log(' vestige eat https://docs.rs/tauri/latest/'); - console.log(' vestige eat ./README.md'); - console.log(' vestige eat ~/Documents/notes.txt'); - console.log('\n The Man Page Absorber chunks content intelligently and'); - console.log(' creates interconnected knowledge nodes for retrieval.\n'); - break; - } - - try { - await eatContent(source, db); - } catch (error) { - console.error(`\n Error ingesting content: ${error instanceof Error ? error.message : 'Unknown error'}\n`); - } - break; - } - - case 'context': { - console.log('\n Ghost in the Shell - Current Context\n'); - const context = captureContext(); - - if (context.activeWindow) { - console.log(` Active App: ${context.activeWindow.app}`); - console.log(` Window Title: ${context.activeWindow.title}`); - } else { - console.log(' Active Window: (unable to detect)'); - } - - console.log(` Working Dir: ${context.workingDirectory}`); - - if (context.gitBranch) { - console.log(` Git Branch: ${context.gitBranch}`); - } - - if (context.recentFiles.length > 0) { - console.log('\n Recent Files (last hour):'); - for (const file of context.recentFiles.slice(0, 5)) { - console.log(` - ${file}`); - } - } - - if (context.clipboard) { - console.log('\n Clipboard:'); - const preview = context.clipboard.slice(0, 200); - console.log(` "${preview}${context.clipboard.length > 200 ? '...' : ''}"`); - } - - console.log('\n Injection String:'); - console.log(` ${formatContextForInjection(context)}`); - console.log(); - break; - } - - case 'watch': { - console.log('\n Starting Ghost in the Shell context watcher...'); - console.log(' Press Ctrl+C to stop.\n'); - - startContextWatcher(5000); - - // Keep running until interrupted - process.on('SIGINT', () => { - console.log('\n Stopping context watcher...'); - process.exit(0); - }); - - // Keep the process alive - await new Promise(() => {}); // Never resolves - break; - } - - case 'dream': { - console.log('\n REM Cycle - Discovering Hidden Connections\n'); - - // Preview first - console.log(' Analyzing knowledge graph...'); - const preview = await previewREMCycle(db); - - if (preview.connectionsDiscovered === 0) { - console.log(' No new connections discovered.'); - console.log(' Your knowledge graph is well-connected or needs more nodes.\n'); - break; - } - - console.log(` Found ${preview.connectionsDiscovered} potential connections!\n`); - - // Show previews - console.log(' Discoveries:'); - for (const d of preview.discoveries.slice(0, 10)) { - console.log(` "${d.nodeA}..."`); - console.log(` <-> "${d.nodeB}..."`); - console.log(` Reason: ${d.reason}\n`); - } - - if (preview.discoveries.length > 10) { - console.log(` ... and ${preview.discoveries.length - 10} more\n`); - } - - // Actually create the connections - if (args[0] !== '--dry-run') { - console.log(' Creating connections...'); - const result = await runREMCycle(db); - console.log(` Created ${result.connectionsCreated} new edges in ${result.duration}ms\n`); - } else { - console.log(' (Dry run - no connections created. Remove --dry-run to create them)\n'); - } - break; - } - - case 'rem': { - // Alias for dream - console.log('\n Starting REM Cycle (alias for "dream")...\n'); - const result = await runREMCycle(db); - - console.log(` Analyzed: ${result.nodesAnalyzed} nodes`); - console.log(` Discovered: ${result.connectionsDiscovered} connections`); - console.log(` Created: ${result.connectionsCreated} edges`); - console.log(` Duration: ${result.duration}ms\n`); - - if (result.discoveries.length > 0) { - console.log(' New connections:'); - for (const d of result.discoveries.slice(0, 5)) { - console.log(` - ${d.reason}`); - } - if (result.discoveries.length > 5) { - console.log(` ... and ${result.discoveries.length - 5} more`); - } - } - console.log(); - break; - } - - // ==================================================================== - // SHADOW SELF - Unsolved Problems Queue - // ==================================================================== - - case 'problem': { - const description = args.join(' '); - if (!description) { - console.log('\n Usage: vestige problem '); - console.log('\n Log an unsolved problem for your Shadow to work on.\n'); - console.log(' Examples:'); - console.log(' vestige problem "How to implement efficient graph traversal"'); - console.log(' vestige problem "Why is the memory leak happening in the worker"'); - console.log('\n The Shadow Self will periodically revisit these problems'); - console.log(' when new knowledge might provide insights.\n'); - break; - } - - const shadow = new ShadowSelf(); - try { - const problem = shadow.logProblem(description, { - context: formatContextForInjection(captureContext()), - priority: 3, - }); - console.log('\n Problem logged to the Shadow Self\n'); - console.log(` ID: ${problem.id}`); - console.log(` Description: ${problem.description.slice(0, 60)}${problem.description.length > 60 ? '...' : ''}`); - console.log(` Priority: ${problem.priority}`); - console.log(` Status: ${problem.status}`); - console.log('\n Your Shadow will work on this while you rest.\n'); - } finally { - shadow.close(); - } - break; - } - - case 'problems': { - const shadow = new ShadowSelf(); - try { - const problems = shadow.getOpenProblems(); - const stats = shadow.getStats(); - - console.log('\n Shadow Self - Unsolved Problems Queue\n'); - console.log(` Total: ${stats.total} | Open: ${stats.open} | Investigating: ${stats.investigating} | Solved: ${stats.solved}\n`); - - if (problems.length === 0) { - console.log(' No open problems. Your mind is at peace.\n'); - console.log(' Log a problem with: vestige problem ""\n'); - break; - } - - for (const p of problems) { - const priority = '!'.repeat(p.priority); - const daysSince = Math.floor((Date.now() - p.createdAt.getTime()) / (1000 * 60 * 60 * 24)); - console.log(` [${p.id.slice(0, 8)}] ${priority.padEnd(5)} ${p.description.slice(0, 50)}${p.description.length > 50 ? '...' : ''}`); - console.log(` Status: ${p.status} | Attempts: ${p.attempts} | Age: ${daysSince}d`); - - // Show any insights - const insights = shadow.getInsights(p.id); - if (insights.length > 0) { - console.log(` Latest insight: "${insights[0]?.insight.slice(0, 40)}..."`); - } - console.log(); - } - } finally { - shadow.close(); - } - break; - } - - case 'solve': { - const problemId = args[0]; - const solution = args.slice(1).join(' '); - - if (!problemId) { - console.log('\n Usage: vestige solve '); - console.log('\n Mark a problem as solved with the solution.\n'); - console.log(' Example:'); - console.log(' vestige solve abc123 "Used memoization to optimize the traversal"'); - console.log('\n Use "vestige problems" to see problem IDs.\n'); - break; - } - - const shadow = new ShadowSelf(); - try { - // Find the problem (match on prefix) - const problems = shadow.getOpenProblems(); - const match = problems.find(p => p.id.startsWith(problemId)); - - if (!match) { - console.log(`\n Problem not found: ${problemId}`); - console.log(' Use "vestige problems" to see open problems.\n'); - break; - } - - shadow.markSolved(match.id, solution || 'Solved (no details provided)'); - console.log('\n Problem marked as SOLVED\n'); - console.log(` Problem: ${match.description.slice(0, 50)}...`); - console.log(` Solution: ${solution || '(no details)'}`); - console.log(` Attempts: ${match.attempts}`); - console.log('\n The Shadow rejoices.\n'); - } finally { - shadow.close(); - } - break; - } - - case 'shadow': { - console.log('\n Shadow Self - Running Background Analysis\n'); - - const shadow = new ShadowSelf(); - try { - const stats = shadow.getStats(); - console.log(` Problems: ${stats.open} open, ${stats.investigating} investigating, ${stats.solved} solved`); - console.log(` Total insights generated: ${stats.totalInsights}\n`); - - if (stats.open === 0 && stats.investigating === 0) { - console.log(' No problems to work on. The Shadow rests.\n'); - break; - } - - console.log(' Running shadow cycle...'); - const result = runShadowCycle(shadow, db); - - console.log(` Analyzed: ${result.problemsAnalyzed} problems`); - console.log(` New insights: ${result.insightsGenerated}\n`); - - if (result.insights.length > 0) { - console.log(' Discoveries:'); - for (const i of result.insights) { - console.log(` Problem: "${i.problem}..."`); - console.log(` Insight: ${i.insight}\n`); - } - } else { - console.log(' No new insights yet. The Shadow continues to watch.\n'); - } - } finally { - shadow.close(); - } - break; - } - - case 'help': - default: - console.log(` - Vestige CLI - Git Blame for AI Thoughts - - Usage: vestige [options] - - Core Commands: - ingest Store knowledge directly - recall Search memories - review Review memories due for reinforcement - stats [detailed] Show memory statistics - - Memory Processing: - dream Run REM cycle (connection discovery) - consolidate Run sleep consolidation (alias: sleep) - decay Apply memory decay - - Embeddings: - embeddings status Check embedding service availability - embeddings generate Generate embeddings for all nodes - embeddings search Semantic similarity search - - Configuration: - config show Display current configuration - config set Update a configuration value - config reset Reset to default configuration - test Run self-tests - - Ghost in the Shell: - context Show current system context - watch Start context watcher daemon - eat Ingest docs/content - - Shadow Self (Unsolved Problems): - problem Log a new unsolved problem - problems List all open problems - solve Mark a problem as solved - shadow Run shadow cycle for insights - - Maintenance: - backup Create database backup - backups List available backups - restore Restore from backup - optimize Optimize database - health Detailed health check - people List people in your network - - Examples: - vestige ingest "API rate limit is 100 req/min" - vestige recall "rate limit" - vestige stats detailed - vestige embeddings search "authentication" - vestige config set logging.level debug - vestige eat https://docs.example.com/api - - The Vestige MCP server runs automatically when connected to Claude. - Your brain gets smarter while you sleep. -`); - } - } finally { - db.close(); - } -} - -function getStatusEmoji(status: string): string { - switch (status) { - case 'healthy': - return '(healthy)'; - case 'warning': - return '(warning)'; - case 'critical': - return '(CRITICAL)'; - default: - return ''; - } -} - -main().catch((error) => { - console.error('Error:', error instanceof Error ? error.message : error); - process.exit(1); -}); diff --git a/packages/core/src/core/config.ts b/packages/core/src/core/config.ts deleted file mode 100644 index 09d733f..0000000 --- a/packages/core/src/core/config.ts +++ /dev/null @@ -1,489 +0,0 @@ -/** - * Configuration Management for Vestige MCP - * - * Provides centralized configuration with: - * - Zod schema validation - * - File-based configuration (~/.vestige/config.json) - * - Environment variable overrides - * - Type-safe accessors for all config sections - * - * Configuration priority (highest to lowest): - * 1. Environment variables - * 2. Config file - * 3. Default values - */ - -import { z } from 'zod'; -import path from 'path'; -import os from 'os'; -import fs from 'fs'; - -// ============================================================================ -// CONFIGURATION SCHEMA -// ============================================================================ - -/** - * Database configuration schema - */ -const DatabaseConfigSchema = z.object({ - /** Path to the SQLite database file */ - path: z.string().default(path.join(os.homedir(), '.vestige', 'vestige.db')), - /** Directory for database backups */ - backupDir: z.string().default(path.join(os.homedir(), '.vestige', 'backups')), - /** SQLite busy timeout in milliseconds */ - busyTimeout: z.number().default(5000), - /** SQLite cache size in pages (negative = KB) */ - cacheSize: z.number().default(64000), - /** Maximum number of backup files to retain */ - maxBackups: z.number().default(5), -}).default({}); - -/** - * FSRS (Free Spaced Repetition Scheduler) algorithm configuration - * Named with 'Config' prefix to avoid collision with FSRSConfigSchema in fsrs.ts - */ -const ConfigFSRSSchema = z.object({ - /** Target retention rate (0.7 to 0.99) */ - desiredRetention: z.number().min(0.7).max(0.99).default(0.9), - /** Custom FSRS-5 weights (19 values). If not provided, uses defaults. */ - weights: z.array(z.number()).length(19).optional(), - /** Enable personalized scheduling based on review history */ - enablePersonalization: z.boolean().default(false), -}).default({}); - -/** - * Dual-strength memory model configuration - * Based on the distinction between storage strength and retrieval strength - */ -const MemoryConfigSchema = z.object({ - /** Storage strength boost on passive access (read) */ - storageBoostOnAccess: z.number().default(0.05), - /** Storage strength boost on active review */ - storageBoostOnReview: z.number().default(0.1), - /** Half-life for retrieval strength decay in days */ - retrievalDecayHalfLife: z.number().default(7), - /** Minimum retention strength before memory is considered weak */ - minRetentionStrength: z.number().default(0.1), -}).default({}); - -/** - * Sentiment analysis configuration for emotional memory weighting - */ -const SentimentConfigSchema = z.object({ - /** Stability multiplier for highly emotional memories */ - stabilityBoost: z.number().default(2.0), - /** Minimum boost applied to any memory */ - minBoost: z.number().default(1.0), -}).default({}); - -/** - * REM (Rapid Eye Movement) cycle configuration - * Handles memory consolidation and connection discovery - */ -const REMConfigSchema = z.object({ - /** Enable REM cycle processing */ - enabled: z.boolean().default(true), - /** Maximum number of memories to analyze per cycle */ - maxAnalyze: z.number().default(50), - /** Minimum connection strength to create an edge */ - minConnectionStrength: z.number().default(0.3), - /** Half-life for temporal proximity weighting in days */ - temporalHalfLifeDays: z.number().default(7), - /** Decay factor for spreading activation (0-1) */ - spreadingActivationDecay: z.number().default(0.8), -}).default({}); - -/** - * Memory consolidation configuration - * Controls the background process that strengthens important memories - */ -const ConsolidationConfigSchema = z.object({ - /** Enable automatic consolidation */ - enabled: z.boolean().default(true), - /** Hour of day to run consolidation (0-23) */ - scheduleHour: z.number().min(0).max(23).default(3), - /** Window in hours for short-term memory processing */ - shortTermWindowHours: z.number().default(24), - /** Minimum importance score for consolidation */ - importanceThreshold: z.number().default(0.5), - /** Threshold below which memories may be pruned */ - pruneThreshold: z.number().default(0.2), -}).default({}); - -/** - * Embeddings service configuration - */ -const EmbeddingsConfigSchema = z.object({ - /** Embedding provider to use */ - provider: z.enum(['ollama', 'fallback']).default('ollama'), - /** Ollama API host URL */ - ollamaHost: z.string().default('http://localhost:11434'), - /** Embedding model name */ - model: z.string().default('nomic-embed-text'), - /** Maximum text length to embed (characters) */ - maxTextLength: z.number().default(8000), -}).default({}); - -/** - * Vector store configuration for semantic search - */ -const VectorStoreConfigSchema = z.object({ - /** Vector store provider */ - provider: z.enum(['chromadb', 'sqlite']).default('chromadb'), - /** ChromaDB host URL */ - chromaHost: z.string().default('http://localhost:8000'), - /** Name of the embeddings collection */ - collectionName: z.string().default('vestige_embeddings'), -}).default({}); - -/** - * Cache configuration - */ -const CacheConfigSchema = z.object({ - /** Enable caching */ - enabled: z.boolean().default(true), - /** Maximum number of items in cache */ - maxSize: z.number().default(10000), - /** Default time-to-live in milliseconds */ - defaultTTLMs: z.number().default(5 * 60 * 1000), -}).default({}); - -/** - * Logging configuration - */ -const LoggingConfigSchema = z.object({ - /** Minimum log level */ - level: z.enum(['debug', 'info', 'warn', 'error']).default('info'), - /** Use structured JSON logging */ - structured: z.boolean().default(true), -}).default({}); - -/** - * Input/output limits configuration - */ -const LimitsConfigSchema = z.object({ - /** Maximum content length in characters */ - maxContentLength: z.number().default(1_000_000), - /** Maximum name/title length in characters */ - maxNameLength: z.number().default(500), - /** Maximum query length in characters */ - maxQueryLength: z.number().default(10_000), - /** Maximum number of tags per item */ - maxTagsCount: z.number().default(100), - /** Maximum items per batch operation */ - maxBatchSize: z.number().default(1000), - /** Default pagination limit */ - paginationDefault: z.number().default(50), - /** Maximum pagination limit */ - paginationMax: z.number().default(500), -}).default({}); - -/** - * Main configuration schema combining all sections - */ -const ConfigSchema = z.object({ - database: DatabaseConfigSchema, - fsrs: ConfigFSRSSchema, - memory: MemoryConfigSchema, - sentiment: SentimentConfigSchema, - rem: REMConfigSchema, - consolidation: ConsolidationConfigSchema, - embeddings: EmbeddingsConfigSchema, - vectorStore: VectorStoreConfigSchema, - cache: CacheConfigSchema, - logging: LoggingConfigSchema, - limits: LimitsConfigSchema, -}); - -/** - * Inferred TypeScript type from the Zod schema - */ -export type VestigeConfig = z.infer; - -// ============================================================================ -// CONFIGURATION LOADING -// ============================================================================ - -/** - * Singleton configuration instance - */ -let config: VestigeConfig | null = null; - -/** - * Partial configuration type for environment overrides - */ -interface PartialVestigeConfig { - database?: { - path?: string; - backupDir?: string; - }; - logging?: { - level?: string; - }; - embeddings?: { - ollamaHost?: string; - model?: string; - }; - vectorStore?: { - chromaHost?: string; - }; - fsrs?: { - desiredRetention?: number; - }; - rem?: { - enabled?: boolean; - }; - consolidation?: { - enabled?: boolean; - }; -} - -/** - * Load environment variable overrides - * Environment variables take precedence over file configuration - */ -function loadEnvConfig(): PartialVestigeConfig { - const env: PartialVestigeConfig = {}; - - // Database configuration - const dbPath = process.env['VESTIGE_DB_PATH']; - const backupDir = process.env['VESTIGE_BACKUP_DIR']; - if (dbPath || backupDir) { - env.database = {}; - if (dbPath) env.database.path = dbPath; - if (backupDir) env.database.backupDir = backupDir; - } - - // Logging configuration - const logLevel = process.env['VESTIGE_LOG_LEVEL']; - if (logLevel) { - env.logging = { level: logLevel }; - } - - // Embeddings configuration - const ollamaHost = process.env['OLLAMA_HOST']; - const embeddingModel = process.env['VESTIGE_EMBEDDING_MODEL']; - if (ollamaHost || embeddingModel) { - env.embeddings = {}; - if (ollamaHost) env.embeddings.ollamaHost = ollamaHost; - if (embeddingModel) env.embeddings.model = embeddingModel; - } - - // Vector store configuration - const chromaHost = process.env['CHROMA_HOST']; - if (chromaHost) { - env.vectorStore = { chromaHost }; - } - - // FSRS configuration - const desiredRetention = process.env['VESTIGE_DESIRED_RETENTION']; - if (desiredRetention) { - const retention = parseFloat(desiredRetention); - if (!isNaN(retention)) { - env.fsrs = { desiredRetention: retention }; - } - } - - // REM configuration - const remEnabled = process.env['VESTIGE_REM_ENABLED']; - if (remEnabled) { - const enabled = remEnabled.toLowerCase() === 'true'; - env.rem = { enabled }; - } - - // Consolidation configuration - const consolidationEnabled = process.env['VESTIGE_CONSOLIDATION_ENABLED']; - if (consolidationEnabled) { - const enabled = consolidationEnabled.toLowerCase() === 'true'; - env.consolidation = { enabled }; - } - - return env; -} - -/** - * Deep merge two objects, with source taking precedence - */ -function deepMerge>(target: T, source: Partial): T { - const result = { ...target }; - - for (const key of Object.keys(source) as (keyof T)[]) { - const sourceValue = source[key]; - const targetValue = result[key]; - - if ( - sourceValue !== undefined && - typeof sourceValue === 'object' && - sourceValue !== null && - !Array.isArray(sourceValue) && - typeof targetValue === 'object' && - targetValue !== null && - !Array.isArray(targetValue) - ) { - result[key] = deepMerge( - targetValue as Record, - sourceValue as Record - ) as T[keyof T]; - } else if (sourceValue !== undefined) { - result[key] = sourceValue as T[keyof T]; - } - } - - return result; -} - -/** - * Load configuration from file and environment variables - * - * @param customPath - Optional custom path to config file - * @returns Validated configuration object - */ -export function loadConfig(customPath?: string): VestigeConfig { - if (config) return config; - - const configPath = customPath || path.join(os.homedir(), '.vestige', 'config.json'); - let fileConfig: Record = {}; - - // Load from file if it exists - if (fs.existsSync(configPath)) { - try { - const content = fs.readFileSync(configPath, 'utf-8'); - fileConfig = JSON.parse(content) as Record; - } catch (error) { - console.warn(`Failed to load config from ${configPath}:`, error); - } - } - - // Load environment variable overrides - const envConfig = loadEnvConfig(); - - // Merge configs: file config first, then env overrides - const mergedConfig = deepMerge(fileConfig, envConfig as Record); - - // Validate and parse with Zod (applies defaults) - config = ConfigSchema.parse(mergedConfig); - - return config; -} - -/** - * Get the current configuration, loading it if necessary - * - * @returns The current configuration object - */ -export function getConfig(): VestigeConfig { - if (!config) { - return loadConfig(); - } - return config; -} - -/** - * Reset the configuration singleton (useful for testing) - */ -export function resetConfig(): void { - config = null; -} - -// ============================================================================ -// CONFIGURATION ACCESSORS -// ============================================================================ - -/** - * Get database configuration - */ -export const getDatabaseConfig = () => getConfig().database; - -/** - * Get FSRS algorithm configuration - */ -export const getFSRSConfig = () => getConfig().fsrs; - -/** - * Get memory model configuration - */ -export const getMemoryConfig = () => getConfig().memory; - -/** - * Get sentiment analysis configuration - */ -export const getSentimentConfig = () => getConfig().sentiment; - -/** - * Get REM cycle configuration - */ -export const getREMConfig = () => getConfig().rem; - -/** - * Get consolidation configuration - */ -export const getConsolidationConfig = () => getConfig().consolidation; - -/** - * Get embeddings service configuration - */ -export const getEmbeddingsConfig = () => getConfig().embeddings; - -/** - * Get vector store configuration - */ -export const getVectorStoreConfig = () => getConfig().vectorStore; - -/** - * Get cache configuration - */ -export const getCacheConfig = () => getConfig().cache; - -/** - * Get logging configuration - */ -export const getLoggingConfig = () => getConfig().logging; - -/** - * Get limits configuration - */ -export const getLimitsConfig = () => getConfig().limits; - -// ============================================================================ -// CONFIGURATION VALIDATION -// ============================================================================ - -/** - * Validate an unknown config object against the schema - * - * @param configObj - Unknown object to validate - * @returns Validated configuration object - * @throws ZodError if validation fails - */ -export function validateConfig(configObj: unknown): VestigeConfig { - return ConfigSchema.parse(configObj); -} - -/** - * Get the Zod schema for configuration validation - * - * @returns The Zod configuration schema - */ -export function getConfigSchema() { - return ConfigSchema; -} - -// ============================================================================ -// EXPORTS -// ============================================================================ - -// Export individual schemas for external use -export { - ConfigSchema, - DatabaseConfigSchema, - ConfigFSRSSchema, - MemoryConfigSchema, - SentimentConfigSchema, - REMConfigSchema, - ConsolidationConfigSchema, - EmbeddingsConfigSchema, - VectorStoreConfigSchema, - CacheConfigSchema, - LoggingConfigSchema, - LimitsConfigSchema, -}; diff --git a/packages/core/src/core/consolidation.ts b/packages/core/src/core/consolidation.ts deleted file mode 100644 index 904139d..0000000 --- a/packages/core/src/core/consolidation.ts +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Sleep Consolidation Simulation - * - * "The brain that consolidates while you sleep." - * - * This module simulates how the human brain consolidates memories during sleep. - * Based on cognitive science research on memory consolidation, it implements: - * - * KEY FEATURES: - * 1. Short-term Memory Processing - Identifies recent memories for consolidation - * 2. Importance-based Promotion - Promotes significant memories to long-term storage - * 3. REM Cycle Integration - Discovers new connections via semantic analysis - * 4. Synaptic Homeostasis - Prunes weak connections to prevent memory overload - * 5. Decay Application - Applies natural memory decay based on forgetting curve - * - * COGNITIVE SCIENCE BASIS: - * - Active Systems Consolidation: Hippocampus replays memories during sleep - * - Synaptic Homeostasis Hypothesis: Weak connections are pruned during sleep - * - Emotional Memory Enhancement: Emotional memories are preferentially consolidated - * - Spreading Activation: Related memories are co-activated and strengthened - */ - -import { VestigeDatabase } from './database.js'; -import { runREMCycle } from './rem-cycle.js'; -import type { KnowledgeNode } from './types.js'; -import { logger } from '../utils/logger.js'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface ConsolidationResult { - /** Number of short-term memories processed */ - shortTermProcessed: number; - /** Number of memories promoted to long-term storage */ - promotedToLongTerm: number; - /** Number of new connections discovered via REM cycle */ - connectionsDiscovered: number; - /** Number of weak edges pruned (synaptic homeostasis) */ - edgesPruned: number; - /** Number of memories that had decay applied */ - decayApplied: number; - /** Duration of consolidation cycle in milliseconds */ - duration: number; -} - -export interface ConsolidationOptions { - /** Hours to look back for short-term memories. Default: 24 */ - shortTermWindowHours?: number; - /** Minimum importance score to promote to long-term. Default: 0.5 */ - importanceThreshold?: number; - /** Edge weight below which connections are pruned. Default: 0.2 */ - pruneThreshold?: number; - /** Maximum number of memories to analyze in REM cycle. Default: 100 */ - maxAnalyze?: number; -} - -// ============================================================================ -// CONSTANTS -// ============================================================================ - -/** Default short-term memory window (24 hours) */ -const DEFAULT_SHORT_TERM_WINDOW_HOURS = 24; - -/** Default importance threshold for long-term promotion */ -const DEFAULT_IMPORTANCE_THRESHOLD = 0.5; - -/** Default edge weight threshold for pruning */ -const DEFAULT_PRUNE_THRESHOLD = 0.2; - -/** Default max memories to analyze */ -const DEFAULT_MAX_ANALYZE = 100; - -/** Weight factors for importance calculation */ -const EMOTION_WEIGHT = 0.4; -const ACCESS_WEIGHT = 0.3; -const CONNECTION_WEIGHT = 0.3; - -/** Maximum values for normalization */ -const MAX_ACCESSES_FOR_IMPORTANCE = 5; -const MAX_CONNECTIONS_FOR_IMPORTANCE = 5; - -// ============================================================================ -// HELPER FUNCTIONS -// ============================================================================ - -/** - * Get memories created within the short-term window - * These are candidates for consolidation processing - */ -async function getShortTermMemories( - db: VestigeDatabase, - windowHours: number -): Promise { - const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000); - const recentNodes = db.getRecentNodes({ limit: 500 }).items; - return recentNodes.filter(node => node.createdAt >= windowStart); -} - -/** - * Calculate importance score for a memory - * - * Importance = f(emotion, access_count, connection_count) - * - * The formula weights three factors: - * - Emotional intensity (40%): Emotionally charged memories are more important - * - Access count (30%): Frequently accessed memories are more important - * - Connection count (30%): Well-connected memories are more important - * - * @returns Importance score from 0 to 1 - */ -function calculateImportance(db: VestigeDatabase, memory: KnowledgeNode): number { - // Get connection count for this memory - const connections = db.getRelatedNodes(memory.id, 1).length; - - // Get emotional intensity (0 to 1) - const emotion = memory.sentimentIntensity || 0; - - // Get access count - const accesses = memory.accessCount; - - // Weighted importance formula - // Each component is normalized to 0-1 range - const emotionScore = emotion * EMOTION_WEIGHT; - const accessScore = - (Math.min(MAX_ACCESSES_FOR_IMPORTANCE, accesses) / MAX_ACCESSES_FOR_IMPORTANCE) * - ACCESS_WEIGHT; - const connectionScore = - (Math.min(MAX_CONNECTIONS_FOR_IMPORTANCE, connections) / MAX_CONNECTIONS_FOR_IMPORTANCE) * - CONNECTION_WEIGHT; - - const importanceScore = emotionScore + accessScore + connectionScore; - - return importanceScore; -} - -/** - * Promote a memory to long-term storage - * - * This boosts the storage strength proportional to importance. - * Based on the Dual-Strength Memory Model (Bjork & Bjork, 1992), - * storage strength represents how well the memory is encoded. - * - * Boost factor ranges from 1x (importance=0) to 3x (importance=1) - */ -async function promoteToLongTerm( - db: VestigeDatabase, - nodeId: string, - importance: number -): Promise { - // Calculate boost factor: 1x to 3x based on importance - const boost = 1 + importance * 2; - - // Access the internal database connection - // Note: This uses internal access pattern for direct SQL operations - const internalDb = (db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db; - - internalDb - .prepare( - ` - UPDATE knowledge_nodes - SET storage_strength = storage_strength * ?, - stability_factor = stability_factor * ? - WHERE id = ? - ` - ) - .run(boost, boost, nodeId); -} - -/** - * Prune weak connections discovered by REM cycle - * - * This implements synaptic homeostasis - the brain's process of - * removing weak synaptic connections during sleep to: - * 1. Prevent memory overload - * 2. Improve signal-to-noise ratio - * 3. Conserve metabolic resources - * - * Only auto-discovered connections (from REM cycle) are pruned. - * User-created connections are preserved regardless of weight. - */ -async function pruneWeakConnections( - db: VestigeDatabase, - threshold: number -): Promise { - // Access the internal database connection - const internalDb = (db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => { changes: number } } } }).db; - - // Remove edges below threshold that were auto-discovered by REM cycle - const result = internalDb - .prepare( - ` - DELETE FROM graph_edges - WHERE weight < ? - AND json_extract(metadata, '$.discoveredBy') = 'rem_cycle' - ` - ) - .run(threshold); - - return result.changes; -} - -// ============================================================================ -// MAIN CONSOLIDATION FUNCTION -// ============================================================================ - -/** - * Run Sleep Consolidation Simulation - * - * Based on cognitive science research on memory consolidation: - * - * PHASE 1: Identify short-term memories - * - Collect memories created within the specified window - * - These represent the "inbox" of memories to process - * - * PHASE 2: Calculate importance and promote - * - Score each memory based on emotion, access, connections - * - Memories above threshold are "promoted" (strengthened) - * - This simulates hippocampal replay during sleep - * - * PHASE 3: Run REM cycle for connection discovery - * - Analyze memories for semantic similarity - * - Discover new connections between related memories - * - Apply spreading activation for transitive connections - * - * PHASE 4: Prune weak connections (synaptic homeostasis) - * - Remove auto-discovered edges below weight threshold - * - Preserves signal-to-noise ratio in memory network - * - * PHASE 5: Apply decay to all memories - * - Apply Ebbinghaus forgetting curve - * - Emotional memories decay slower - * - Well-encoded memories (high storage strength) decay slower - * - * @param db - VestigeDatabase instance - * @param options - Consolidation configuration options - * @returns Results of the consolidation cycle - */ -export async function runConsolidation( - db: VestigeDatabase, - options: ConsolidationOptions = {} -): Promise { - const startTime = Date.now(); - const { - shortTermWindowHours = DEFAULT_SHORT_TERM_WINDOW_HOURS, - importanceThreshold = DEFAULT_IMPORTANCE_THRESHOLD, - pruneThreshold = DEFAULT_PRUNE_THRESHOLD, - maxAnalyze = DEFAULT_MAX_ANALYZE, - } = options; - - const result: ConsolidationResult = { - shortTermProcessed: 0, - promotedToLongTerm: 0, - connectionsDiscovered: 0, - edgesPruned: 0, - decayApplied: 0, - duration: 0, - }; - - logger.info('Starting consolidation cycle', { - shortTermWindowHours, - importanceThreshold, - pruneThreshold, - maxAnalyze, - }); - - // PHASE 1: Identify short-term memories - // These are memories created within the window that need processing - const shortTermMemories = await getShortTermMemories(db, shortTermWindowHours); - result.shortTermProcessed = shortTermMemories.length; - - logger.debug('Phase 1: Identified short-term memories', { - count: shortTermMemories.length, - }); - - // PHASE 2: Calculate importance and promote to long-term - // This simulates the hippocampal replay that occurs during sleep - for (const memory of shortTermMemories) { - const importance = calculateImportance(db, memory); - if (importance >= importanceThreshold) { - await promoteToLongTerm(db, memory.id, importance); - result.promotedToLongTerm++; - } - } - - logger.debug('Phase 2: Promoted memories to long-term storage', { - promoted: result.promotedToLongTerm, - threshold: importanceThreshold, - }); - - // PHASE 3: Run REM cycle for connection discovery - // This discovers semantic connections between memories - const remResult = await runREMCycle(db, { maxAnalyze }); - result.connectionsDiscovered = remResult.connectionsCreated; - - logger.debug('Phase 3: REM cycle complete', { - connectionsDiscovered: remResult.connectionsDiscovered, - connectionsCreated: remResult.connectionsCreated, - spreadingActivationEdges: remResult.spreadingActivationEdges, - }); - - // PHASE 4: Prune weak connections (synaptic homeostasis) - // Remove auto-discovered connections that are below the threshold - result.edgesPruned = await pruneWeakConnections(db, pruneThreshold); - - logger.debug('Phase 4: Pruned weak connections', { - edgesPruned: result.edgesPruned, - threshold: pruneThreshold, - }); - - // PHASE 5: Apply decay to all memories - // Uses Ebbinghaus forgetting curve with emotional weighting - result.decayApplied = db.applyDecay(); - - logger.debug('Phase 5: Applied memory decay', { - memoriesAffected: result.decayApplied, - }); - - result.duration = Date.now() - startTime; - logger.info('Consolidation cycle complete', { ...result }); - - return result; -} - -// ============================================================================ -// SCHEDULING HELPER -// ============================================================================ - -/** - * Get recommended next consolidation time - * - * Returns the next occurrence of 3 AM local time. - * This is based on research showing that: - * - Deep sleep (when consolidation occurs) typically happens 3-4 AM - * - System resources are usually free at this time - * - Users are unlikely to be actively using the system - * - * @returns Date object representing the next recommended consolidation time - */ -export function getNextConsolidationTime(): Date { - const now = new Date(); - const next = new Date(now); - - // Schedule for 3 AM next day - next.setDate(next.getDate() + 1); - next.setHours(3, 0, 0, 0); - - return next; -} - -/** - * Preview consolidation results without making changes - * - * Useful for understanding what would happen during consolidation - * without actually modifying the database. - * - * Note: This still runs the analysis phases but skips the - * actual modification phases. - */ -export async function previewConsolidation( - db: VestigeDatabase, - options: ConsolidationOptions = {} -): Promise<{ - shortTermCount: number; - wouldPromote: number; - potentialConnections: number; - weakEdgeCount: number; -}> { - const { - shortTermWindowHours = DEFAULT_SHORT_TERM_WINDOW_HOURS, - importanceThreshold = DEFAULT_IMPORTANCE_THRESHOLD, - pruneThreshold = DEFAULT_PRUNE_THRESHOLD, - maxAnalyze = DEFAULT_MAX_ANALYZE, - } = options; - - // Get short-term memories - const shortTermMemories = await getShortTermMemories(db, shortTermWindowHours); - - // Count how many would be promoted - let wouldPromote = 0; - for (const memory of shortTermMemories) { - const importance = calculateImportance(db, memory); - if (importance >= importanceThreshold) { - wouldPromote++; - } - } - - // Preview REM cycle (dry run) - const remPreview = await runREMCycle(db, { maxAnalyze, dryRun: true }); - - // Count weak edges that would be pruned - const internalDb = (db as unknown as { db: { prepare: (sql: string) => { get: (...args: unknown[]) => { count: number } } } }).db; - const weakEdgeResult = internalDb - .prepare( - ` - SELECT COUNT(*) as count FROM graph_edges - WHERE weight < ? - AND json_extract(metadata, '$.discoveredBy') = 'rem_cycle' - ` - ) - .get(pruneThreshold) as { count: number }; - - return { - shortTermCount: shortTermMemories.length, - wouldPromote, - potentialConnections: remPreview.connectionsDiscovered, - weakEdgeCount: weakEdgeResult.count, - }; -} diff --git a/packages/core/src/core/context-watcher.ts b/packages/core/src/core/context-watcher.ts deleted file mode 100644 index 6c203ef..0000000 --- a/packages/core/src/core/context-watcher.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Ghost in the Shell - Context Watcher - * - * Watches the active window and clipboard to provide contextual awareness. - * Vestige sees what you see. - * - * Features: - * - Active window title detection (macOS via AppleScript) - * - Clipboard monitoring - * - Context file for MCP injection - */ - -import { execSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface SystemContext { - timestamp: string; - activeWindow: { - app: string; - title: string; - } | null; - clipboard: string | null; - workingDirectory: string; - gitBranch: string | null; - recentFiles: string[]; -} - -// ============================================================================ -// CONTEXT FILE LOCATION -// ============================================================================ - -const CONTEXT_FILE = path.join(os.homedir(), '.vestige', 'context.json'); - -// ============================================================================ -// PLATFORM-SPECIFIC IMPLEMENTATIONS -// ============================================================================ - -/** - * Get active window info on macOS using AppleScript - */ -function getActiveWindowMac(): { app: string; title: string } | null { - try { - // Get frontmost app name - const appScript = ` - tell application "System Events" - set frontApp to first application process whose frontmost is true - return name of frontApp - end tell - `; - const app = execSync(`osascript -e '${appScript}'`, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - - // Get window title - const titleScript = ` - tell application "System Events" - tell (first application process whose frontmost is true) - if (count of windows) > 0 then - return name of front window - else - return "" - end if - end tell - end tell - `; - const title = execSync(`osascript -e '${titleScript}'`, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - - return { app, title }; - } catch { - return null; - } -} - -/** - * Get clipboard content on macOS - */ -function getClipboardMac(): string | null { - try { - const content = execSync('pbpaste', { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - maxBuffer: 1024 * 100, // 100KB max - }); - // Truncate long clipboard content - if (content.length > 2000) { - return content.slice(0, 2000) + '\n... [truncated]'; - } - return content || null; - } catch { - return null; - } -} - -/** - * Get current git branch - */ -function getGitBranch(): string | null { - try { - return execSync('git rev-parse --abbrev-ref HEAD', { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - } catch { - return null; - } -} - -/** - * Get recently modified files in current directory - */ -function getRecentFiles(): string[] { - try { - // Get files modified in last hour, sorted by time - const result = execSync( - 'find . -type f -mmin -60 -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" 2>/dev/null | head -10', - { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - } - ); - return result - .split('\n') - .map(f => f.trim()) - .filter(Boolean) - .slice(0, 10); - } catch { - return []; - } -} - -// ============================================================================ -// CONTEXT CAPTURE -// ============================================================================ - -/** - * Capture current system context - */ -export function captureContext(): SystemContext { - const platform = process.platform; - - let activeWindow: { app: string; title: string } | null = null; - let clipboard: string | null = null; - - if (platform === 'darwin') { - activeWindow = getActiveWindowMac(); - clipboard = getClipboardMac(); - } - // TODO: Add Windows and Linux support - - return { - timestamp: new Date().toISOString(), - activeWindow, - clipboard, - workingDirectory: process.cwd(), - gitBranch: getGitBranch(), - recentFiles: getRecentFiles(), - }; -} - -/** - * Format context for injection into Claude prompts - */ -export function formatContextForInjection(context: SystemContext): string { - const parts: string[] = []; - - if (context.activeWindow) { - parts.push(`Active: ${context.activeWindow.app} - ${context.activeWindow.title}`); - } - - if (context.gitBranch) { - parts.push(`Git: ${context.gitBranch}`); - } - - if (context.recentFiles.length > 0) { - parts.push(`Recent: ${context.recentFiles.slice(0, 3).join(', ')}`); - } - - if (context.clipboard && context.clipboard.length < 500) { - parts.push(`Clipboard: "${context.clipboard.slice(0, 200)}${context.clipboard.length > 200 ? '...' : ''}"`); - } - - return parts.join(' | '); -} - -/** - * Save context to file for external consumption - */ -export function saveContext(context: SystemContext): void { - try { - const dir = path.dirname(CONTEXT_FILE); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - fs.writeFileSync(CONTEXT_FILE, JSON.stringify(context, null, 2)); - } catch { - // Ignore file write errors - } -} - -/** - * Read saved context from file - */ -export function readSavedContext(): SystemContext | null { - try { - if (fs.existsSync(CONTEXT_FILE)) { - const content = fs.readFileSync(CONTEXT_FILE, 'utf-8'); - return JSON.parse(content) as SystemContext; - } - } catch { - // Ignore read errors - } - return null; -} - -// ============================================================================ -// WATCHER DAEMON -// ============================================================================ - -let watcherInterval: NodeJS.Timeout | null = null; - -/** - * Start the context watcher daemon - * Updates context every N seconds - */ -export function startContextWatcher(intervalMs: number = 5000): void { - if (watcherInterval) { - console.log('Context watcher already running'); - return; - } - - console.log(`Starting context watcher (interval: ${intervalMs}ms)`); - - // Capture immediately - const context = captureContext(); - saveContext(context); - - // Then update periodically - watcherInterval = setInterval(() => { - const ctx = captureContext(); - saveContext(ctx); - }, intervalMs); -} - -/** - * Stop the context watcher daemon - */ -export function stopContextWatcher(): void { - if (watcherInterval) { - clearInterval(watcherInterval); - watcherInterval = null; - console.log('Context watcher stopped'); - } -} - -/** - * Check if watcher is running - */ -export function isWatcherRunning(): boolean { - return watcherInterval !== null; -} diff --git a/packages/core/src/core/database.ts b/packages/core/src/core/database.ts deleted file mode 100644 index 72f7644..0000000 --- a/packages/core/src/core/database.ts +++ /dev/null @@ -1,1975 +0,0 @@ -import Database from 'better-sqlite3'; -import { nanoid } from 'nanoid'; -import path from 'path'; -import fs from 'fs'; -import os from 'os'; -import crypto from 'crypto'; -import { execSync } from 'child_process'; -import natural from 'natural'; -import type { KnowledgeNode, KnowledgeNodeInput, PersonNode, GraphEdge, Source, Interaction } from './types.js'; - -// ============================================================================ -// SENTIMENT ANALYSIS (Emotional Memory Weighting) -// ============================================================================ - -// Initialize sentiment analyzer -// We use AFINN for simplicity - it assigns scores from -5 to +5 for emotional words -const Analyzer = natural.SentimentAnalyzer; -const stemmer = natural.PorterStemmer; -const tokenizer = new natural.WordTokenizer(); -const sentimentAnalyzer = new Analyzer('English', stemmer, 'afinn'); - -/** - * Analyze the emotional intensity of text content - * Returns a value from 0 (neutral) to 1 (highly emotional) - * - * KEY INSIGHT: We care about INTENSITY, not polarity - * "I'm absolutely THRILLED" and "I'm completely DEVASTATED" should both decay slowly - * because they're emotionally significant memories - */ -export function analyzeSentimentIntensity(content: string): number { - try { - const tokens = tokenizer.tokenize(content.toLowerCase()); - if (!tokens || tokens.length === 0) return 0; - - // Get raw sentiment score (-1 to 1 typical range) - const rawScore = sentimentAnalyzer.getSentiment(tokens); - - // Convert to INTENSITY (absolute value, normalized) - // We also boost based on emotional word density - const absScore = Math.abs(rawScore); - - // Count emotional words (those contributing to sentiment) - // AFINN-165 has ~2500 words with sentiment values - let emotionalWordCount = 0; - for (const token of tokens) { - // If the token contributed to score, it's emotional - const singleTokenScore = sentimentAnalyzer.getSentiment([token]); - if (singleTokenScore !== 0) { - emotionalWordCount++; - } - } - - // Emotional density = emotional words / total words - const emotionalDensity = emotionalWordCount / tokens.length; - - // Combine raw intensity with density - // More emotional words = more memorable content - const combinedIntensity = (absScore * 0.6) + (emotionalDensity * 0.4); - - // Normalize to 0-1 range (cap at 1) - return Math.min(1, Math.max(0, combinedIntensity)); - } catch { - // If sentiment analysis fails, return neutral - return 0; - } -} - -// ============================================================================ -// GIT-BLAME FOR THOUGHTS - Capture code context when memory is created -// ============================================================================ - -export interface GitContext { - branch?: string; - commit?: string; - commitMessage?: string; - repoPath?: string; - dirty?: boolean; - changedFiles?: string[]; -} - -/** - * Capture current git context - what code is being worked on right now? - * This allows "time travel" to see what you were coding when you had a thought. - * - * Example use case: - * - You're debugging a nasty race condition - * - You have an insight and use `ingest` to record it - * - Later, you recall the insight and see: "Branch: fix/race-condition, Commit: abc123" - * - This context helps you understand WHY you had that thought - */ -export function captureGitContext(): GitContext | undefined { - try { - const cwd = process.cwd(); - - // Check if we're in a git repository - try { - execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'pipe' }); - } catch { - return undefined; // Not a git repo - } - - const context: GitContext = {}; - - // Get repo root - try { - context.repoPath = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim(); - } catch { - // Ignore - } - - // Get current branch - try { - context.branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf-8' }).trim(); - } catch { - // Ignore - } - - // Get current commit (short SHA) - try { - context.commit = execSync('git rev-parse --short HEAD', { cwd, encoding: 'utf-8' }).trim(); - } catch { - // Ignore - } - - // Get commit message (first line) - try { - context.commitMessage = execSync('git log -1 --format=%s', { cwd, encoding: 'utf-8' }).trim(); - // Truncate long messages - if (context.commitMessage.length > 100) { - context.commitMessage = context.commitMessage.slice(0, 97) + '...'; - } - } catch { - // Ignore - } - - // Check for uncommitted changes - try { - const status = execSync('git status --porcelain', { cwd, encoding: 'utf-8' }).trim(); - context.dirty = status.length > 0; - if (context.dirty) { - // Get list of changed files (limit to 10) - const files = status.split('\n') - .map(line => line.slice(3).trim()) - .filter(Boolean) - .slice(0, 10); - if (files.length > 0) { - context.changedFiles = files; - } - } - } catch { - // Ignore - } - - return context; - } catch { - // Git context capture is optional - never fail ingestion - return undefined; - } -} - -// ============================================================================ -// CONSTANTS & CONFIGURATION -// ============================================================================ - -const DEFAULT_DB_PATH = path.join(os.homedir(), '.vestige', 'vestige.db'); -const BACKUP_DIR = path.join(os.homedir(), '.vestige', 'backups'); - -// Size thresholds (in bytes) -const SIZE_WARNING_THRESHOLD = 100 * 1024 * 1024; // 100MB -const SIZE_CRITICAL_THRESHOLD = 500 * 1024 * 1024; // 500MB -const MAX_NODES_WARNING = 50000; -const MAX_NODES_CRITICAL = 100000; - -// Default pagination -const DEFAULT_LIMIT = 50; -const MAX_LIMIT = 500; - -// Input validation limits -const MAX_CONTENT_LENGTH = 1_000_000; // 1MB max content -const MAX_NAME_LENGTH = 500; // 500 chars for names -const MAX_QUERY_LENGTH = 10_000; // 10KB max query -const MAX_TAGS_COUNT = 100; // Max tags per node -const MAX_BATCH_SIZE = 1000; // Max items in batch operations - -// Concurrency control -const BUSY_TIMEOUT_MS = 5000; // 5 second busy timeout - -// SM-2 Spaced Repetition Constants -const SM2_EASE_FACTOR = 2.5; // Standard SM-2 ease factor for successful recall -const SM2_LAPSE_THRESHOLD = 0.3; // Below this retention = "forgot" (lapse) -const SM2_MIN_STABILITY = 1.0; // Minimum stability (1 day) -const SM2_MAX_STABILITY = 365.0; // Maximum stability (1 year - effectively permanent) - -// Sentiment-Weighted Decay Constants -const SENTIMENT_STABILITY_BOOST = 2.0; // Max 2x stability boost for highly emotional memories -const SENTIMENT_MIN_BOOST = 1.0; // Neutral content gets no boost - -// ============================================================================ -// SECURITY HELPERS -// ============================================================================ - -/** - * Validate that a path is within an allowed directory (prevents path traversal) - */ -function isPathWithinDirectory(targetPath: string, allowedDir: string): boolean { - const resolvedTarget = path.resolve(targetPath); - const resolvedAllowed = path.resolve(allowedDir); - return resolvedTarget.startsWith(resolvedAllowed + path.sep) || resolvedTarget === resolvedAllowed; -} - -/** - * Validate backup file path - must be within BACKUP_DIR and have .db extension - */ -function validateBackupPath(backupPath: string): void { - const resolvedPath = path.resolve(backupPath); - const resolvedBackupDir = path.resolve(BACKUP_DIR); - - // Check path is within backup directory - if (!isPathWithinDirectory(resolvedPath, resolvedBackupDir)) { - throw new VestigeDatabaseError( - 'Backup path must be within the backup directory', - 'INVALID_BACKUP_PATH' - ); - } - - // Validate file extension - if (!resolvedPath.endsWith('.db')) { - throw new VestigeDatabaseError( - 'Backup file must have .db extension', - 'INVALID_BACKUP_EXTENSION' - ); - } - - // Check for null bytes or other suspicious characters - if (backupPath.includes('\0') || backupPath.includes('..')) { - throw new VestigeDatabaseError( - 'Invalid characters in backup path', - 'INVALID_BACKUP_PATH' - ); - } -} - -/** - * Safe JSON parse with fallback - never throws - */ -function safeJsonParse(value: string | null | undefined, fallback: T): T { - if (!value) return fallback; - try { - const parsed = JSON.parse(value); - // Basic type validation - if (typeof parsed !== typeof fallback) { - return fallback; - } - return parsed as T; - } catch { - return fallback; - } -} - -/** - * Sanitize error message to prevent sensitive data leakage - */ -function sanitizeErrorMessage(message: string): string { - // Remove file paths - let sanitized = message.replace(/\/[^\s]+/g, '[PATH]'); - // Remove potential SQL queries - sanitized = sanitized.replace(/SELECT|INSERT|UPDATE|DELETE|DROP|CREATE/gi, '[SQL]'); - // Remove potential connection strings - sanitized = sanitized.replace(/\b(password|secret|key|token|auth)\s*[=:]\s*\S+/gi, '[REDACTED]'); - return sanitized; -} - -/** - * Validate string length for inputs - */ -function validateStringLength(value: string, maxLength: number, fieldName: string): void { - if (value && value.length > maxLength) { - throw new VestigeDatabaseError( - `${fieldName} exceeds maximum length of ${maxLength} characters`, - 'INPUT_TOO_LONG' - ); - } -} - -/** - * Validate array length for inputs - */ -function validateArrayLength(arr: T[] | undefined, maxLength: number, fieldName: string): void { - if (arr && arr.length > maxLength) { - throw new VestigeDatabaseError( - `${fieldName} exceeds maximum count of ${maxLength} items`, - 'INPUT_TOO_MANY_ITEMS' - ); - } -} - -// ============================================================================ -// ERROR TYPES -// ============================================================================ - -export class VestigeDatabaseError extends Error { - constructor( - message: string, - public readonly code: string, - cause?: unknown - ) { - // Sanitize the message to prevent sensitive data leakage - super(sanitizeErrorMessage(message)); - this.name = 'VestigeDatabaseError'; - // Don't expose the original cause in production - it may contain sensitive info - if (process.env.NODE_ENV === 'development' && cause) { - this.cause = cause; - } - } -} - -// ============================================================================ -// HEALTH CHECK TYPES -// ============================================================================ - -export interface HealthStatus { - status: 'healthy' | 'warning' | 'critical'; - dbPath: string; - dbSizeBytes: number; - dbSizeMB: number; - nodeCount: number; - peopleCount: number; - edgeCount: number; - walMode: boolean; - integrityCheck: boolean; - warnings: string[]; - lastBackup: string | null; -} - -export interface PaginationOptions { - limit?: number; - offset?: number; -} - -export interface PaginatedResult { - items: T[]; - total: number; - limit: number; - offset: number; - hasMore: boolean; -} - -// ============================================================================ -// DATABASE INITIALIZATION -// ============================================================================ - -export function getDbPath(): string { - const envPath = process.env['VESTIGE_DB_PATH']; - return envPath || DEFAULT_DB_PATH; -} - -export function initializeDatabase(dbPath?: string): Database.Database { - const finalPath = dbPath || getDbPath(); - - // Ensure directory exists - const dir = path.dirname(finalPath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - // Ensure backup directory exists - if (!fs.existsSync(BACKUP_DIR)) { - fs.mkdirSync(BACKUP_DIR, { recursive: true }); - } - - const db = new Database(finalPath); - - // CRITICAL: Set busy timeout FIRST to handle concurrent access - // This prevents SQLITE_BUSY errors when multiple clients access the DB - db.pragma(`busy_timeout = ${BUSY_TIMEOUT_MS}`); - - // Enable WAL mode for better concurrent performance - db.pragma('journal_mode = WAL'); - db.pragma('foreign_keys = ON'); - - // Optimize for performance - db.pragma('synchronous = NORMAL'); - db.pragma('cache_size = -64000'); // 64MB cache - db.pragma('temp_store = MEMORY'); - - // Security: Limit memory usage to prevent DoS - db.pragma('max_page_count = 1073741823'); // ~2TB max (practical limit) - - // Enable secure delete to overwrite deleted data - db.pragma('secure_delete = ON'); - - // Create tables - createTables(db); - - // Run migrations for existing databases - runMigrations(db); - - return db; -} - -function createTables(db: Database.Database): void { - // Knowledge Nodes table - db.exec(` - CREATE TABLE IF NOT EXISTS knowledge_nodes ( - id TEXT PRIMARY KEY, - content TEXT NOT NULL, - summary TEXT, - - -- Temporal metadata - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - last_accessed_at TEXT NOT NULL, - access_count INTEGER DEFAULT 0, - - -- Decay modeling (SM-2 inspired spaced repetition) - retention_strength REAL DEFAULT 1.0, - stability_factor REAL DEFAULT 1.0, -- Grows with successful reviews, resets on lapse - sentiment_intensity REAL DEFAULT 0, -- Emotional weight (0=neutral, 1=highly emotional) - next_review_date TEXT, - review_count INTEGER DEFAULT 0, - - -- Dual-Strength Memory Model (Bjork & Bjork, 1992) - storage_strength REAL DEFAULT 1.0, -- How well encoded (never decreases) - retrieval_strength REAL DEFAULT 1.0, -- How accessible now (decays) - - -- Provenance - source_type TEXT NOT NULL, - source_platform TEXT NOT NULL, - source_id TEXT, - source_url TEXT, - source_chain TEXT DEFAULT '[]', -- JSON array - git_context TEXT, -- JSON object: {branch, commit, commitMessage, repoPath, dirty, changedFiles} - - -- Confidence - confidence REAL DEFAULT 0.8, - is_contradicted INTEGER DEFAULT 0, - contradiction_ids TEXT DEFAULT '[]', -- JSON array - - -- Extracted entities (JSON arrays) - people TEXT DEFAULT '[]', - concepts TEXT DEFAULT '[]', - events TEXT DEFAULT '[]', - tags TEXT DEFAULT '[]' - ); - - -- Indexes for common queries - CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON knowledge_nodes(created_at); - CREATE INDEX IF NOT EXISTS idx_nodes_last_accessed ON knowledge_nodes(last_accessed_at); - CREATE INDEX IF NOT EXISTS idx_nodes_retention ON knowledge_nodes(retention_strength); - CREATE INDEX IF NOT EXISTS idx_nodes_source_type ON knowledge_nodes(source_type); - CREATE INDEX IF NOT EXISTS idx_nodes_source_platform ON knowledge_nodes(source_platform); - `); - - // Full-text search for content - db.exec(` - CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5( - id, - content, - summary, - tags, - content='knowledge_nodes', - content_rowid='rowid' - ); - - -- Triggers to keep FTS in sync - CREATE TRIGGER IF NOT EXISTS knowledge_ai AFTER INSERT ON knowledge_nodes BEGIN - INSERT INTO knowledge_fts(rowid, id, content, summary, tags) - VALUES (NEW.rowid, NEW.id, NEW.content, NEW.summary, NEW.tags); - END; - - CREATE TRIGGER IF NOT EXISTS knowledge_ad AFTER DELETE ON knowledge_nodes BEGIN - INSERT INTO knowledge_fts(knowledge_fts, rowid, id, content, summary, tags) - VALUES ('delete', OLD.rowid, OLD.id, OLD.content, OLD.summary, OLD.tags); - END; - - CREATE TRIGGER IF NOT EXISTS knowledge_au AFTER UPDATE ON knowledge_nodes BEGIN - INSERT INTO knowledge_fts(knowledge_fts, rowid, id, content, summary, tags) - VALUES ('delete', OLD.rowid, OLD.id, OLD.content, OLD.summary, OLD.tags); - INSERT INTO knowledge_fts(rowid, id, content, summary, tags) - VALUES (NEW.rowid, NEW.id, NEW.content, NEW.summary, NEW.tags); - END; - `); - - // People table - db.exec(` - CREATE TABLE IF NOT EXISTS people ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - aliases TEXT DEFAULT '[]', -- JSON array - - -- Relationship context - how_we_met TEXT, - relationship_type TEXT, - organization TEXT, - role TEXT, - location TEXT, - - -- Contact info - email TEXT, - phone TEXT, - social_links TEXT DEFAULT '{}', -- JSON object - - -- Communication patterns - last_contact_at TEXT, - contact_frequency REAL DEFAULT 0, - preferred_channel TEXT, - - -- Shared context - shared_topics TEXT DEFAULT '[]', - shared_projects TEXT DEFAULT '[]', - - -- Meta - notes TEXT, - relationship_health REAL DEFAULT 0.5, - - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_people_name ON people(name); - CREATE INDEX IF NOT EXISTS idx_people_last_contact ON people(last_contact_at); - `); - - // Interactions table - db.exec(` - CREATE TABLE IF NOT EXISTS interactions ( - id TEXT PRIMARY KEY, - person_id TEXT NOT NULL, - type TEXT NOT NULL, - date TEXT NOT NULL, - summary TEXT NOT NULL, - topics TEXT DEFAULT '[]', - sentiment REAL, - action_items TEXT DEFAULT '[]', - source_node_id TEXT, - - FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE, - FOREIGN KEY (source_node_id) REFERENCES knowledge_nodes(id) ON DELETE SET NULL - ); - - CREATE INDEX IF NOT EXISTS idx_interactions_person ON interactions(person_id); - CREATE INDEX IF NOT EXISTS idx_interactions_date ON interactions(date); - `); - - // Graph edges table - db.exec(` - CREATE TABLE IF NOT EXISTS graph_edges ( - id TEXT PRIMARY KEY, - from_id TEXT NOT NULL, - to_id TEXT NOT NULL, - edge_type TEXT NOT NULL, - weight REAL DEFAULT 0.5, - metadata TEXT DEFAULT '{}', - created_at TEXT NOT NULL, - - UNIQUE(from_id, to_id, edge_type) - ); - - CREATE INDEX IF NOT EXISTS idx_edges_from ON graph_edges(from_id); - CREATE INDEX IF NOT EXISTS idx_edges_to ON graph_edges(to_id); - CREATE INDEX IF NOT EXISTS idx_edges_type ON graph_edges(edge_type); - `); - - // Sources table - db.exec(` - CREATE TABLE IF NOT EXISTS sources ( - id TEXT PRIMARY KEY, - type TEXT NOT NULL, - platform TEXT NOT NULL, - original_id TEXT, - url TEXT, - file_path TEXT, - title TEXT, - author TEXT, - publication_date TEXT, - - ingested_at TEXT NOT NULL, - last_synced_at TEXT NOT NULL, - content_hash TEXT, - - node_count INTEGER DEFAULT 0 - ); - - CREATE INDEX IF NOT EXISTS idx_sources_platform ON sources(platform); - CREATE INDEX IF NOT EXISTS idx_sources_file_path ON sources(file_path); - `); - - // Embeddings reference table (actual vectors stored in ChromaDB) - db.exec(` - CREATE TABLE IF NOT EXISTS embeddings ( - node_id TEXT PRIMARY KEY, - chroma_id TEXT NOT NULL, - model TEXT NOT NULL, - created_at TEXT NOT NULL, - - FOREIGN KEY (node_id) REFERENCES knowledge_nodes(id) ON DELETE CASCADE - ); - `); - - // Metadata table for tracking backups and system info - db.exec(` - CREATE TABLE IF NOT EXISTS vestige_metadata ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - `); -} - -/** - * Run database migrations for existing databases - * This ensures older databases get new columns - */ -function runMigrations(db: Database.Database): void { - try { - const columns = db.prepare("PRAGMA table_info(knowledge_nodes)").all() as { name: string }[]; - - // Migration 1: Add stability_factor column if it doesn't exist - const hasStabilityFactor = columns.some(col => col.name === 'stability_factor'); - if (!hasStabilityFactor) { - db.exec(` - ALTER TABLE knowledge_nodes - ADD COLUMN stability_factor REAL DEFAULT 1.0; - `); - - // Initialize stability based on review_count for existing nodes - // Nodes that have been reviewed multiple times should have higher stability - db.exec(` - UPDATE knowledge_nodes - SET stability_factor = MIN(${SM2_MAX_STABILITY}, POWER(${SM2_EASE_FACTOR}, review_count)) - WHERE review_count > 0; - `); - } - - // Migration 2: Add sentiment_intensity column if it doesn't exist - const hasSentimentIntensity = columns.some(col => col.name === 'sentiment_intensity'); - if (!hasSentimentIntensity) { - db.exec(` - ALTER TABLE knowledge_nodes - ADD COLUMN sentiment_intensity REAL DEFAULT 0; - `); - - // Backfill sentiment for existing nodes - // This analyzes all existing content and sets sentiment intensity - const nodes = db.prepare('SELECT id, content FROM knowledge_nodes').all() as { id: string; content: string }[]; - const updateStmt = db.prepare('UPDATE knowledge_nodes SET sentiment_intensity = ? WHERE id = ?'); - - for (const node of nodes) { - const intensity = analyzeSentimentIntensity(node.content); - if (intensity > 0) { - updateStmt.run(intensity, node.id); - } - } - } - - // Migration 3: Add git_context column if it doesn't exist - const hasGitContext = columns.some(col => col.name === 'git_context'); - if (!hasGitContext) { - db.exec(` - ALTER TABLE knowledge_nodes - ADD COLUMN git_context TEXT; - `); - // No backfill - we can't retroactively determine git context - } - - // Migration 4: Add Dual-Strength Memory Model columns (Bjork & Bjork, 1992) - const hasStorageStrength = columns.some(col => col.name === 'storage_strength'); - if (!hasStorageStrength) { - db.exec(` - ALTER TABLE knowledge_nodes - ADD COLUMN storage_strength REAL DEFAULT 1.0; - `); - - // Backfill storage_strength based on review history - // storage_strength = 1.0 + (review_count * 0.5) + (access_count * 0.1) - db.exec(` - UPDATE knowledge_nodes - SET storage_strength = 1.0 + (review_count * 0.5) + (access_count * 0.1); - `); - } - - const hasRetrievalStrength = columns.some(col => col.name === 'retrieval_strength'); - if (!hasRetrievalStrength) { - db.exec(` - ALTER TABLE knowledge_nodes - ADD COLUMN retrieval_strength REAL DEFAULT 1.0; - `); - - // Backfill retrieval_strength from retention_strength for backward compatibility - db.exec(` - UPDATE knowledge_nodes - SET retrieval_strength = retention_strength; - `); - } - } catch { - // Migration may have already been applied or table doesn't exist yet - } -} - -// ============================================================================ -// CRUD OPERATIONS -// ============================================================================ - -/** - * Simple mutex for serializing critical database operations - */ -class OperationMutex { - private locked = false; - private queue: (() => void)[] = []; - - async acquire(): Promise { - return new Promise((resolve) => { - if (!this.locked) { - this.locked = true; - resolve(); - } else { - this.queue.push(resolve); - } - }); - } - - release(): void { - const next = this.queue.shift(); - if (next) { - next(); - } else { - this.locked = false; - } - } -} - -export class VestigeDatabase { - private db: Database.Database; - private dbPath: string; - private readonly writeMutex = new OperationMutex(); - - constructor(dbPath?: string) { - this.dbPath = dbPath || getDbPath(); - this.db = initializeDatabase(this.dbPath); - } - - // ============================================================================ - // HEALTH & MONITORING - // ============================================================================ - - /** - * Get comprehensive health status of the database - */ - checkHealth(): HealthStatus { - const warnings: string[] = []; - let status: 'healthy' | 'warning' | 'critical' = 'healthy'; - - // Get database file size - let dbSizeBytes = 0; - try { - const stats = fs.statSync(this.dbPath); - dbSizeBytes = stats.size; - - // Also check WAL file size - const walPath = this.dbPath + '-wal'; - if (fs.existsSync(walPath)) { - const walStats = fs.statSync(walPath); - dbSizeBytes += walStats.size; - } - } catch { - warnings.push('Could not determine database file size'); - } - - const dbSizeMB = dbSizeBytes / (1024 * 1024); - - // Size warnings - if (dbSizeBytes >= SIZE_CRITICAL_THRESHOLD) { - status = 'critical'; - warnings.push(`Database size (${dbSizeMB.toFixed(1)}MB) exceeds critical threshold (${SIZE_CRITICAL_THRESHOLD / 1024 / 1024}MB)`); - } else if (dbSizeBytes >= SIZE_WARNING_THRESHOLD) { - status = 'warning'; - warnings.push(`Database size (${dbSizeMB.toFixed(1)}MB) exceeds warning threshold (${SIZE_WARNING_THRESHOLD / 1024 / 1024}MB)`); - } - - // Get counts - const stats = this.getStats(); - - // Node count warnings - if (stats.totalNodes >= MAX_NODES_CRITICAL) { - status = 'critical'; - warnings.push(`Node count (${stats.totalNodes}) exceeds critical threshold (${MAX_NODES_CRITICAL})`); - } else if (stats.totalNodes >= MAX_NODES_WARNING) { - if (status !== 'critical') status = 'warning'; - warnings.push(`Node count (${stats.totalNodes}) exceeds warning threshold (${MAX_NODES_WARNING})`); - } - - // Check WAL mode - const journalMode = this.db.pragma('journal_mode', { simple: true }) as string; - const walMode = journalMode.toLowerCase() === 'wal'; - if (!walMode) { - if (status === 'healthy') status = 'warning'; - warnings.push('WAL mode is not enabled - concurrent performance may be degraded'); - } - - // Quick integrity check (just checks header, not full scan) - let integrityCheck = true; - try { - const result = this.db.pragma('quick_check', { simple: true }) as string; - integrityCheck = result === 'ok'; - if (!integrityCheck) { - status = 'critical'; - warnings.push('Database integrity check failed'); - } - } catch (e) { - integrityCheck = false; - status = 'critical'; - warnings.push('Could not run integrity check'); - } - - // Get last backup time - let lastBackup: string | null = null; - try { - const row = this.db.prepare('SELECT value FROM vestige_metadata WHERE key = ?').get('last_backup') as { value: string } | undefined; - lastBackup = row?.value || null; - - // Warn if no backup in 7 days - if (lastBackup) { - const lastBackupDate = new Date(lastBackup); - const daysSinceBackup = (Date.now() - lastBackupDate.getTime()) / (1000 * 60 * 60 * 24); - if (daysSinceBackup > 7) { - if (status === 'healthy') status = 'warning'; - warnings.push(`Last backup was ${Math.floor(daysSinceBackup)} days ago`); - } - } else { - if (status === 'healthy') status = 'warning'; - warnings.push('No backup has been created'); - } - } catch { - // Metadata table might not exist in older databases - } - - return { - status, - dbPath: this.dbPath, - dbSizeBytes, - dbSizeMB, - nodeCount: stats.totalNodes, - peopleCount: stats.totalPeople, - edgeCount: stats.totalEdges, - walMode, - integrityCheck, - warnings, - lastBackup, - }; - } - - /** - * Get database size in bytes - */ - getDatabaseSize(): { bytes: number; mb: number; formatted: string } { - let bytes = 0; - try { - const stats = fs.statSync(this.dbPath); - bytes = stats.size; - - // Include WAL file - const walPath = this.dbPath + '-wal'; - if (fs.existsSync(walPath)) { - bytes += fs.statSync(walPath).size; - } - } catch { - // File might not exist yet - } - - const mb = bytes / (1024 * 1024); - const formatted = mb < 1 ? `${(bytes / 1024).toFixed(1)}KB` : `${mb.toFixed(1)}MB`; - - return { bytes, mb, formatted }; - } - - // ============================================================================ - // BACKUP & RESTORE - // ============================================================================ - - /** - * Create a backup of the database - * @returns Path to the backup file - */ - backup(customPath?: string): string { - // Generate safe backup filename with timestamp - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const backupFileName = `vestige-backup-${timestamp}.db`; - - // Determine backup path - always force it to be within BACKUP_DIR for security - let backupPath: string; - if (customPath) { - // SECURITY: Validate custom path is within backup directory - const resolvedCustom = path.resolve(customPath); - const resolvedBackupDir = path.resolve(BACKUP_DIR); - - // If custom path is just a filename, place it in BACKUP_DIR - if (!customPath.includes(path.sep) && !customPath.includes('/')) { - backupPath = path.join(BACKUP_DIR, customPath); - } else if (isPathWithinDirectory(resolvedCustom, resolvedBackupDir)) { - backupPath = resolvedCustom; - } else { - throw new VestigeDatabaseError( - 'Custom backup path must be within the backup directory', - 'INVALID_BACKUP_PATH' - ); - } - validateBackupPath(backupPath); - } else { - backupPath = path.join(BACKUP_DIR, backupFileName); - } - - // Checkpoint WAL to ensure all data is in main file - this.db.pragma('wal_checkpoint(TRUNCATE)'); - - // Ensure backup directory exists - const backupDir = path.dirname(backupPath); - if (!fs.existsSync(backupDir)) { - fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 }); // Restrict permissions - } - - // Use file copy for backup (simpler and synchronous) - fs.copyFileSync(this.dbPath, backupPath); - - // Set restrictive permissions on backup file - try { - fs.chmodSync(backupPath, 0o600); // Owner read/write only - } catch { - // chmod may not work on all platforms, continue anyway - } - - // Update metadata - const now = new Date().toISOString(); - this.db.prepare(` - INSERT OR REPLACE INTO vestige_metadata (key, value, updated_at) - VALUES (?, ?, ?) - `).run('last_backup', now, now); - - // Clean old backups (keep last 5) - this.cleanOldBackups(5); - - return backupPath; - } - - /** - * List available backups - */ - listBackups(): { path: string; size: number; date: Date }[] { - if (!fs.existsSync(BACKUP_DIR)) { - return []; - } - - const files = fs.readdirSync(BACKUP_DIR) - .filter(f => f.startsWith('vestige-backup-') && f.endsWith('.db')) - .map(f => { - const fullPath = path.join(BACKUP_DIR, f); - const stats = fs.statSync(fullPath); - return { - path: fullPath, - size: stats.size, - date: stats.mtime, - }; - }) - .sort((a, b) => b.date.getTime() - a.date.getTime()); - - return files; - } - - /** - * Restore from a backup file - * WARNING: This will replace the current database! - * - * SECURITY: Only accepts backups from the official backup directory - */ - restore(backupPath: string): void { - // CRITICAL SECURITY: Validate backup path is within backup directory - // This prevents path traversal attacks (CWE-22) - validateBackupPath(backupPath); - - const resolvedPath = path.resolve(backupPath); - - if (!fs.existsSync(resolvedPath)) { - throw new VestigeDatabaseError( - 'Backup file not found', - 'BACKUP_NOT_FOUND' - ); - } - - // Validate the backup file is actually a SQLite database - try { - const header = Buffer.alloc(16); - const fd = fs.openSync(resolvedPath, 'r'); - fs.readSync(fd, header, 0, 16, 0); - fs.closeSync(fd); - - // SQLite database files start with "SQLite format 3\0" - const sqliteHeader = 'SQLite format 3\0'; - if (header.toString('utf8', 0, 16) !== sqliteHeader) { - throw new VestigeDatabaseError( - 'Invalid backup file format - not a valid SQLite database', - 'INVALID_BACKUP_FORMAT' - ); - } - } catch (error) { - if (error instanceof VestigeDatabaseError) throw error; - throw new VestigeDatabaseError( - 'Failed to validate backup file', - 'BACKUP_VALIDATION_FAILED' - ); - } - - // Close current connection - this.db.close(); - - // Create a backup of current database before restoring - const preRestoreBackup = this.dbPath + '.pre-restore-' + Date.now(); - if (fs.existsSync(this.dbPath)) { - fs.copyFileSync(this.dbPath, preRestoreBackup); - } - - try { - // Copy backup to database location - fs.copyFileSync(resolvedPath, this.dbPath); - - // Remove WAL files if they exist - const walPath = this.dbPath + '-wal'; - const shmPath = this.dbPath + '-shm'; - if (fs.existsSync(walPath)) fs.unlinkSync(walPath); - if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath); - - // Reopen database and verify integrity - this.db = initializeDatabase(this.dbPath); - - // Verify the restored database has the expected schema - const tables = this.db.prepare( - "SELECT name FROM sqlite_master WHERE type='table'" - ).all() as { name: string }[]; - const tableNames = tables.map(t => t.name); - - if (!tableNames.includes('knowledge_nodes') || !tableNames.includes('people')) { - throw new Error('Restored database is missing required tables'); - } - - // Clean up pre-restore backup on success - if (fs.existsSync(preRestoreBackup)) { - fs.unlinkSync(preRestoreBackup); - } - } catch (error) { - // Restore failed, try to recover - if (fs.existsSync(preRestoreBackup)) { - fs.copyFileSync(preRestoreBackup, this.dbPath); - this.db = initializeDatabase(this.dbPath); - fs.unlinkSync(preRestoreBackup); - } - throw new VestigeDatabaseError( - 'Failed to restore backup', - 'RESTORE_FAILED' - ); - } - } - - /** - * Clean old backups, keeping only the most recent N - */ - private cleanOldBackups(keepCount: number): void { - const backups = this.listBackups(); - const toDelete = backups.slice(keepCount); - - for (const backup of toDelete) { - try { - fs.unlinkSync(backup.path); - } catch { - // Ignore deletion errors - } - } - } - - // ============================================================================ - // KNOWLEDGE NODES - // ============================================================================ - - insertNode(node: Omit): KnowledgeNode { - try { - // Input validation - validateStringLength(node.content, MAX_CONTENT_LENGTH, 'Content'); - validateStringLength(node.summary || '', MAX_CONTENT_LENGTH, 'Summary'); - validateArrayLength(node.tags, MAX_TAGS_COUNT, 'Tags'); - validateArrayLength(node.people, MAX_TAGS_COUNT, 'People'); - validateArrayLength(node.concepts, MAX_TAGS_COUNT, 'Concepts'); - validateArrayLength(node.events, MAX_TAGS_COUNT, 'Events'); - - // Validate confidence is within bounds - const confidence = Math.max(0, Math.min(1, node.confidence ?? 0.8)); - const retention = Math.max(0, Math.min(1, node.retentionStrength ?? 1.0)); - - // Dual-Strength Memory Model (Bjork & Bjork, 1992) - const storageStrength = Math.max(1, node.storageStrength ?? 1.0); - const retrievalStrength = Math.max(0, Math.min(1, node.retrievalStrength ?? 1.0)); - - // Analyze emotional intensity of content - // Highly emotional memories get stability boost (decay slower) - const sentimentIntensity = node.sentimentIntensity ?? analyzeSentimentIntensity(node.content); - - // Git-Blame for Thoughts: Capture current code context - // This lets you "time travel" to see what you were working on when you had this thought - const gitContext = node.gitContext ?? captureGitContext(); - - const id = nanoid(); - const now = new Date().toISOString(); - - const stmt = this.db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, summary, - created_at, updated_at, last_accessed_at, access_count, - retention_strength, sentiment_intensity, next_review_date, review_count, - storage_strength, retrieval_strength, - source_type, source_platform, source_id, source_url, source_chain, git_context, - confidence, is_contradicted, contradiction_ids, - people, concepts, events, tags - ) VALUES ( - ?, ?, ?, - ?, ?, ?, ?, - ?, ?, ?, ?, - ?, ?, - ?, ?, ?, ?, ?, ?, - ?, ?, ?, - ?, ?, ?, ? - ) - `); - - stmt.run( - id, node.content, node.summary || null, - node.createdAt?.toISOString() || now, - now, - now, - 0, - retention, - sentimentIntensity, - node.nextReviewDate?.toISOString() || null, - 0, - storageStrength, - retrievalStrength, - node.sourceType, - node.sourcePlatform, - node.sourceId || null, - node.sourceUrl || null, - JSON.stringify(node.sourceChain || []), - gitContext ? JSON.stringify(gitContext) : null, - confidence, - node.isContradicted ? 1 : 0, - JSON.stringify(node.contradictionIds || []), - JSON.stringify(node.people || []), - JSON.stringify(node.concepts || []), - JSON.stringify(node.events || []), - JSON.stringify(node.tags || []) - ); - - return { ...node, id } as KnowledgeNode; - } catch (error) { - if (error instanceof VestigeDatabaseError) throw error; - throw new VestigeDatabaseError( - 'Failed to insert knowledge node', - 'INSERT_NODE_FAILED' - ); - } - } - - getNode(id: string): KnowledgeNode | null { - try { - const stmt = this.db.prepare('SELECT * FROM knowledge_nodes WHERE id = ?'); - const row = stmt.get(id) as Record | undefined; - if (!row) return null; - return this.rowToNode(row); - } catch (error) { - throw new VestigeDatabaseError( - `Failed to get node: ${id}`, - 'GET_NODE_FAILED', - error - ); - } - } - - updateNodeAccess(id: string): void { - try { - // Dual-Strength Memory Model (Bjork & Bjork, 1992): - // - Storage strength increases with each exposure (never decreases) - // - Retrieval strength resets to 1.0 on access (we just retrieved it successfully) - const stmt = this.db.prepare(` - UPDATE knowledge_nodes - SET last_accessed_at = ?, - access_count = access_count + 1, - storage_strength = storage_strength + 0.05, - retrieval_strength = 1.0 - WHERE id = ? - `); - stmt.run(new Date().toISOString(), id); - } catch (error) { - throw new VestigeDatabaseError( - `Failed to update node access: ${id}`, - 'UPDATE_ACCESS_FAILED', - error - ); - } - } - - /** - * Mark a node as reviewed (spaced repetition) - */ - /** - * Mark a node as reviewed using SM-2 inspired spaced repetition - * - * KEY INSIGHT: We don't just reset retention - we modify the STABILITY FACTOR - * - High retention (remembered easily) → Stability increases → Slower future decay - * - Low retention (forgot/struggled) → Stability resets → Must rebuild memory - * - * This creates "crystallized" memories that barely decay after multiple reviews - * - * DUAL-STRENGTH MODEL (Bjork & Bjork, 1992): - * - Storage strength ALWAYS increases on review (more for difficult recalls) - * - Retrieval strength resets to 1.0 - */ - markReviewed(id: string): void { - try { - const node = this.getNode(id); - if (!node) { - throw new VestigeDatabaseError(`Node not found: ${id}`, 'NODE_NOT_FOUND'); - } - - const currentStability = node.stabilityFactor ?? SM2_MIN_STABILITY; - const currentStorageStrength = node.storageStrength ?? 1.0; - let newStability: number; - let newReviewCount: number; - let newStorageStrength: number; - - // SM-2 with Lapse Detection - if (node.retentionStrength >= SM2_LAPSE_THRESHOLD) { - // SUCCESSFUL RECALL: Memory was still accessible - // Increase stability - the curve gets flatter (slower decay) - newStability = Math.min(SM2_MAX_STABILITY, currentStability * SM2_EASE_FACTOR); - newReviewCount = node.reviewCount + 1; - // Storage strength increases moderately for easy recalls - newStorageStrength = currentStorageStrength + 0.1; - } else { - // LAPSE: Memory had decayed too far - we "forgot" it - // Reset stability - must rebuild the memory from scratch - // But keep review count as a record of total attempts - newStability = SM2_MIN_STABILITY; - newReviewCount = node.reviewCount + 1; // Still count the review - // DESIRABLE DIFFICULTY: Storage strength increases MORE for difficult recalls - // This is a key insight from Bjork & Bjork - struggling to retrieve strengthens encoding - newStorageStrength = currentStorageStrength + 0.3; - } - - // Reset retention to full strength (we just accessed it) - const newRetention = 1.0; - // Reset retrieval strength to 1.0 (we just retrieved it successfully) - const newRetrievalStrength = 1.0; - - // Calculate next review date based on NEW stability - // Higher stability = longer until next review needed - const daysUntilReview = Math.ceil(newStability); - const nextReview = new Date(); - nextReview.setDate(nextReview.getDate() + daysUntilReview); - - const stmt = this.db.prepare(` - UPDATE knowledge_nodes - SET retention_strength = ?, - stability_factor = ?, - review_count = ?, - next_review_date = ?, - last_accessed_at = ?, - updated_at = ?, - storage_strength = ?, - retrieval_strength = ? - WHERE id = ? - `); - stmt.run( - newRetention, - newStability, - newReviewCount, - nextReview.toISOString(), - new Date().toISOString(), - new Date().toISOString(), - newStorageStrength, - newRetrievalStrength, - id - ); - } catch (error) { - if (error instanceof VestigeDatabaseError) throw error; - throw new VestigeDatabaseError( - 'Failed to mark node as reviewed', - 'MARK_REVIEWED_FAILED' - ); - } - } - - searchNodes(query: string, options: PaginationOptions = {}): PaginatedResult { - try { - // Input validation - validateStringLength(query, MAX_QUERY_LENGTH, 'Search query'); - - // Sanitize FTS5 query to prevent injection - // FTS5 special characters: AND OR NOT ( ) " * ^ - const sanitizedQuery = query - .replace(/[^\w\s\-]/g, ' ') // Remove special characters except hyphen - .trim(); - - if (!sanitizedQuery) { - return { - items: [], - total: 0, - limit: DEFAULT_LIMIT, - offset: 0, - hasMore: false, - }; - } - - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(Math.max(1, limit), MAX_LIMIT); - const safeOffset = Math.max(0, offset); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM knowledge_nodes kn - JOIN knowledge_fts fts ON kn.id = fts.id - WHERE knowledge_fts MATCH ? - `); - const countResult = countStmt.get(sanitizedQuery) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT kn.* FROM knowledge_nodes kn - JOIN knowledge_fts fts ON kn.id = fts.id - WHERE knowledge_fts MATCH ? - ORDER BY rank - LIMIT ? OFFSET ? - `); - const rows = stmt.all(sanitizedQuery, safeLimit, safeOffset) as Record[]; - const items = rows.map(row => this.rowToNode(row)); - - return { - items, - total, - limit: safeLimit, - offset: safeOffset, - hasMore: safeOffset + items.length < total, - }; - } catch (error) { - if (error instanceof VestigeDatabaseError) throw error; - throw new VestigeDatabaseError( - 'Search operation failed', - 'SEARCH_FAILED' - ); - } - } - - getRecentNodes(options: PaginationOptions = {}): PaginatedResult { - try { - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(limit, MAX_LIMIT); - - // Get total count - const countResult = this.db.prepare('SELECT COUNT(*) as total FROM knowledge_nodes').get() as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM knowledge_nodes - ORDER BY created_at DESC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(safeLimit, offset) as Record[]; - const items = rows.map(row => this.rowToNode(row)); - - return { - items, - total, - limit: safeLimit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new VestigeDatabaseError( - 'Failed to get recent nodes', - 'GET_RECENT_FAILED', - error - ); - } - } - - getDecayingNodes(threshold: number = 0.5, options: PaginationOptions = {}): PaginatedResult { - try { - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(limit, MAX_LIMIT); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM knowledge_nodes - WHERE retention_strength < ? - `); - const countResult = countStmt.get(threshold) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM knowledge_nodes - WHERE retention_strength < ? - ORDER BY retention_strength ASC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(threshold, safeLimit, offset) as Record[]; - const items = rows.map(row => this.rowToNode(row)); - - return { - items, - total, - limit: safeLimit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new VestigeDatabaseError( - 'Failed to get decaying nodes', - 'GET_DECAYING_FAILED', - error - ); - } - } - - getNodeCount(): number { - try { - const stmt = this.db.prepare('SELECT COUNT(*) as count FROM knowledge_nodes'); - const result = stmt.get() as { count: number }; - return result.count; - } catch (error) { - throw new VestigeDatabaseError( - 'Failed to get node count', - 'COUNT_FAILED', - error - ); - } - } - - /** - * Delete a knowledge node - */ - deleteNode(id: string): boolean { - try { - const stmt = this.db.prepare('DELETE FROM knowledge_nodes WHERE id = ?'); - const result = stmt.run(id); - return result.changes > 0; - } catch (error) { - throw new VestigeDatabaseError( - `Failed to delete node: ${id}`, - 'DELETE_NODE_FAILED', - error - ); - } - } - - // ============================================================================ - // PEOPLE - // ============================================================================ - - insertPerson(person: Omit): PersonNode { - try { - // Input validation - validateStringLength(person.name, MAX_NAME_LENGTH, 'Name'); - validateStringLength(person.notes || '', MAX_CONTENT_LENGTH, 'Notes'); - validateStringLength(person.howWeMet || '', MAX_CONTENT_LENGTH, 'How we met'); - validateArrayLength(person.aliases, MAX_TAGS_COUNT, 'Aliases'); - validateArrayLength(person.sharedTopics, MAX_TAGS_COUNT, 'Shared topics'); - validateArrayLength(person.sharedProjects, MAX_TAGS_COUNT, 'Shared projects'); - - // Validate relationship health is within bounds - const relationshipHealth = Math.max(0, Math.min(1, person.relationshipHealth ?? 0.5)); - - const id = nanoid(); - const now = new Date().toISOString(); - - const stmt = this.db.prepare(` - INSERT INTO people ( - id, name, aliases, - how_we_met, relationship_type, organization, role, location, - email, phone, social_links, - last_contact_at, contact_frequency, preferred_channel, - shared_topics, shared_projects, - notes, relationship_health, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, person.name, JSON.stringify(person.aliases || []), - person.howWeMet || null, person.relationshipType || null, - person.organization || null, person.role || null, person.location || null, - person.email || null, person.phone || null, JSON.stringify(person.socialLinks || {}), - person.lastContactAt?.toISOString() || null, person.contactFrequency || 0, - person.preferredChannel || null, - JSON.stringify(person.sharedTopics || []), JSON.stringify(person.sharedProjects || []), - person.notes || null, relationshipHealth, - now, now - ); - - return { ...person, id, createdAt: new Date(now), updatedAt: new Date(now) } as PersonNode; - } catch (error) { - if (error instanceof VestigeDatabaseError) throw error; - throw new VestigeDatabaseError( - 'Failed to insert person', - 'INSERT_PERSON_FAILED' - ); - } - } - - getPerson(id: string): PersonNode | null { - try { - const stmt = this.db.prepare('SELECT * FROM people WHERE id = ?'); - const row = stmt.get(id) as Record | undefined; - if (!row) return null; - return this.rowToPerson(row); - } catch (error) { - throw new VestigeDatabaseError( - `Failed to get person: ${id}`, - 'GET_PERSON_FAILED', - error - ); - } - } - - getPersonByName(name: string): PersonNode | null { - try { - // Input validation - validateStringLength(name, MAX_NAME_LENGTH, 'Name'); - - // Sanitize name for LIKE query - escape special LIKE characters - // This prevents SQL injection via LIKE wildcards - const escapedName = name - .replace(/\\/g, '\\\\') // Escape backslashes first - .replace(/%/g, '\\%') // Escape percent - .replace(/_/g, '\\_') // Escape underscore - .replace(/"/g, '\\"'); // Escape quotes for JSON match - - const stmt = this.db.prepare(` - SELECT * FROM people - WHERE name = ? OR aliases LIKE ? ESCAPE '\\' - `); - const row = stmt.get(name, `%"${escapedName}"%`) as Record | undefined; - if (!row) return null; - return this.rowToPerson(row); - } catch (error) { - if (error instanceof VestigeDatabaseError) throw error; - throw new VestigeDatabaseError( - 'Failed to get person by name', - 'GET_PERSON_BY_NAME_FAILED' - ); - } - } - - getAllPeople(options: PaginationOptions = {}): PaginatedResult { - try { - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(limit, MAX_LIMIT); - - // Get total count - const countResult = this.db.prepare('SELECT COUNT(*) as total FROM people').get() as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare('SELECT * FROM people ORDER BY name LIMIT ? OFFSET ?'); - const rows = stmt.all(safeLimit, offset) as Record[]; - const items = rows.map(row => this.rowToPerson(row)); - - return { - items, - total, - limit: safeLimit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new VestigeDatabaseError( - 'Failed to get all people', - 'GET_ALL_PEOPLE_FAILED', - error - ); - } - } - - getPeopleToReconnect(daysSinceContact: number = 30, options: PaginationOptions = {}): PaginatedResult { - try { - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(limit, MAX_LIMIT); - - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - daysSinceContact); - const cutoffStr = cutoffDate.toISOString(); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM people - WHERE last_contact_at IS NOT NULL AND last_contact_at < ? - `); - const countResult = countStmt.get(cutoffStr) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM people - WHERE last_contact_at IS NOT NULL - AND last_contact_at < ? - ORDER BY last_contact_at ASC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(cutoffStr, safeLimit, offset) as Record[]; - const items = rows.map(row => this.rowToPerson(row)); - - return { - items, - total, - limit: safeLimit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new VestigeDatabaseError( - 'Failed to get people to reconnect', - 'GET_RECONNECT_FAILED', - error - ); - } - } - - /** - * Update last contact date for a person - */ - updatePersonContact(id: string): void { - try { - const stmt = this.db.prepare(` - UPDATE people - SET last_contact_at = ?, updated_at = ? - WHERE id = ? - `); - const now = new Date().toISOString(); - stmt.run(now, now, id); - } catch (error) { - throw new VestigeDatabaseError( - `Failed to update person contact: ${id}`, - 'UPDATE_CONTACT_FAILED', - error - ); - } - } - - /** - * Delete a person - */ - deletePerson(id: string): boolean { - try { - const stmt = this.db.prepare('DELETE FROM people WHERE id = ?'); - const result = stmt.run(id); - return result.changes > 0; - } catch (error) { - throw new VestigeDatabaseError( - `Failed to delete person: ${id}`, - 'DELETE_PERSON_FAILED', - error - ); - } - } - - // ============================================================================ - // GRAPH EDGES - // ============================================================================ - - insertEdge(edge: Omit): GraphEdge { - try { - const id = nanoid(); - const now = new Date().toISOString(); - - const stmt = this.db.prepare(` - INSERT OR REPLACE INTO graph_edges ( - id, from_id, to_id, edge_type, weight, metadata, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, edge.fromId, edge.toId, edge.edgeType, - edge.weight ?? 0.5, JSON.stringify(edge.metadata || {}), - now - ); - - return { ...edge, id, createdAt: new Date(now) } as GraphEdge; - } catch (error) { - throw new VestigeDatabaseError( - 'Failed to insert edge', - 'INSERT_EDGE_FAILED', - error - ); - } - } - - getRelatedNodes(nodeId: string, depth: number = 1): string[] { - try { - // Simple BFS for related nodes - const visited = new Set(); - let current = [nodeId]; - - for (let d = 0; d < depth; d++) { - if (current.length === 0) break; - - const placeholders = current.map(() => '?').join(','); - const stmt = this.db.prepare(` - SELECT DISTINCT - CASE WHEN from_id IN (${placeholders}) THEN to_id ELSE from_id END as related_id - FROM graph_edges - WHERE from_id IN (${placeholders}) OR to_id IN (${placeholders}) - `); - - const params = [...current, ...current, ...current]; - const rows = stmt.all(...params) as { related_id: string }[]; - - const newNodes: string[] = []; - for (const row of rows) { - if (!visited.has(row.related_id) && row.related_id !== nodeId) { - visited.add(row.related_id); - newNodes.push(row.related_id); - } - } - current = newNodes; - } - - return Array.from(visited); - } catch (error) { - throw new VestigeDatabaseError( - `Failed to get related nodes: ${nodeId}`, - 'GET_RELATED_FAILED', - error - ); - } - } - - // ============================================================================ - // STATS - // ============================================================================ - - getStats(): { totalNodes: number; totalPeople: number; totalEdges: number } { - try { - const nodeCount = this.db.prepare('SELECT COUNT(*) as c FROM knowledge_nodes').get() as { c: number }; - const peopleCount = this.db.prepare('SELECT COUNT(*) as c FROM people').get() as { c: number }; - const edgeCount = this.db.prepare('SELECT COUNT(*) as c FROM graph_edges').get() as { c: number }; - - return { - totalNodes: nodeCount.c, - totalPeople: peopleCount.c, - totalEdges: edgeCount.c, - }; - } catch (error) { - throw new VestigeDatabaseError( - 'Failed to get stats', - 'GET_STATS_FAILED', - error - ); - } - } - - // ============================================================================ - // MAINTENANCE - // ============================================================================ - - /** - * Optimize database (vacuum and reindex) - */ - optimize(): void { - try { - // Checkpoint WAL - this.db.pragma('wal_checkpoint(TRUNCATE)'); - // Vacuum to reclaim space - this.db.exec('VACUUM'); - // Reindex for performance - this.db.exec('REINDEX'); - } catch (error) { - throw new VestigeDatabaseError( - 'Failed to optimize database', - 'OPTIMIZE_FAILED', - error - ); - } - } - - /** - * Apply decay to all nodes based on time since last access - * Call this periodically (e.g., daily) to update retention strengths - * - * KEY FEATURES: - * 1. Each node decays at ITS OWN rate based on stability_factor - * 2. EMOTIONAL MEMORIES DECAY SLOWER via sentiment_intensity boost - * 3. DUAL-STRENGTH MODEL (Bjork & Bjork, 1992): - * - Retrieval strength decays based on storage strength and sentiment - * - Storage strength NEVER decreases (only increases on access/review) - * - Higher storage = slower retrieval decay - * - * Decay rates: - * - New neutral memories (S=1, emotion=0): Decay fast → 50% after 1 day - * - New emotional memories (S=1, emotion=1): Decay slower → 75% after 1 day - * - Reviewed memories (S=10): Decay slow → 90% after 1 day - * - Crystallized emotional (S=100, emotion=1): Near-permanent → 99.5% after 1 day - * - * Uses IMMEDIATE transaction to prevent dirty reads and ensure consistency - */ - applyDecay(): number { - try { - const now = Date.now(); - - // Use IMMEDIATE transaction mode for write consistency - // This acquires write lock at start, preventing concurrent modifications - const transaction = this.db.transaction(() => { - // Fetch all strength factors for each node - const nodes = this.db.prepare(` - SELECT id, last_accessed_at, retention_strength, stability_factor, sentiment_intensity, - storage_strength, retrieval_strength - FROM knowledge_nodes - `).all() as { - id: string; - last_accessed_at: string; - retention_strength: number; - stability_factor: number | null; - sentiment_intensity: number | null; - storage_strength: number | null; - retrieval_strength: number | null; - }[]; - - let updated = 0; - const updateStmt = this.db.prepare(` - UPDATE knowledge_nodes - SET retention_strength = ?, retrieval_strength = ? - WHERE id = ? - `); - - for (const node of nodes) { - const lastAccessed = new Date(node.last_accessed_at).getTime(); - const daysSince = (now - lastAccessed) / (1000 * 60 * 60 * 24); - - // Base stability factor (from SM-2 reviews) - const baseStability = node.stability_factor ?? SM2_MIN_STABILITY; - - // SENTIMENT BOOST: Emotional memories decay slower - // sentimentIntensity: 0 = neutral (1x), 1 = highly emotional (2x boost) - const sentimentIntensity = node.sentiment_intensity ?? 0; - const sentimentMultiplier = SENTIMENT_MIN_BOOST + (sentimentIntensity * (SENTIMENT_STABILITY_BOOST - SENTIMENT_MIN_BOOST)); - - // Effective stability = base stability * sentiment boost - // A memory with S=10 and high emotion (1.0) becomes S_effective = 10 * 2 = 20 - const effectiveStability = baseStability * sentimentMultiplier; - - // Ebbinghaus forgetting curve: R = e^(-t/S) - // where t is time in days and S is effective stability - // Higher S = slower decay = "crystallized" or emotional memory - const newRetention = Math.max(0.1, node.retention_strength * Math.exp(-daysSince / effectiveStability)); - - // DUAL-STRENGTH MODEL (Bjork & Bjork, 1992): - // Retrieval strength decays based on storage strength and sentiment intensity - // Higher storage strength = slower retrieval decay - const storageStrength = node.storage_strength ?? 1.0; - const currentRetrievalStrength = node.retrieval_strength ?? 1.0; - - // Effective decay rate is inversely proportional to storage strength and emotional weight - // Formula: effectiveDecayRate = 1 / (storageStrength * (1 + sentimentIntensity)) - const effectiveDecayRate = 1 / (storageStrength * (1 + sentimentIntensity)); - - // Apply decay to retrieval strength with minimum floor of 0.1 - const newRetrievalStrength = Math.max(0.1, Math.exp(-daysSince * effectiveDecayRate)); - - // Compute backward-compatible retention_strength as a weighted combination - // This preserves existing behavior while incorporating dual-strength model - // retention_strength = (retrieval_strength * 0.7) + (normalized_storage * 0.3) - const normalizedStorage = Math.min(1, storageStrength / 10); - const backwardCompatibleRetention = (newRetrievalStrength * 0.7) + (normalizedStorage * 0.3); - - const hasRetentionChange = Math.abs(backwardCompatibleRetention - node.retention_strength) > 0.01; - const hasRetrievalChange = Math.abs(newRetrievalStrength - currentRetrievalStrength) > 0.01; - - if (hasRetentionChange || hasRetrievalChange) { - updateStmt.run(backwardCompatibleRetention, newRetrievalStrength, node.id); - updated++; - } - } - - return updated; - }); - - // Execute with IMMEDIATE mode (acquires RESERVED lock immediately) - return transaction.immediate(); - } catch (error) { - if (error instanceof VestigeDatabaseError) throw error; - throw new VestigeDatabaseError( - 'Failed to apply decay', - 'APPLY_DECAY_FAILED' - ); - } - } - - // ============================================================================ - // HELPERS - // ============================================================================ - - private rowToNode(row: Record): KnowledgeNode { - // Use safe JSON parsing with fallbacks to prevent crashes from corrupted data - return { - id: row['id'] as string, - content: row['content'] as string, - summary: row['summary'] as string | undefined, - createdAt: new Date(row['created_at'] as string), - updatedAt: new Date(row['updated_at'] as string), - lastAccessedAt: new Date(row['last_accessed_at'] as string), - accessCount: row['access_count'] as number, - retentionStrength: row['retention_strength'] as number, - stabilityFactor: (row['stability_factor'] as number) ?? SM2_MIN_STABILITY, - sentimentIntensity: (row['sentiment_intensity'] as number) ?? 0, - nextReviewDate: row['next_review_date'] ? new Date(row['next_review_date'] as string) : undefined, - reviewCount: row['review_count'] as number, - // Dual-Strength Memory Model (Bjork & Bjork, 1992) - storageStrength: (row['storage_strength'] as number) ?? 1.0, - retrievalStrength: (row['retrieval_strength'] as number) ?? 1.0, - sourceType: row['source_type'] as KnowledgeNode['sourceType'], - sourcePlatform: row['source_platform'] as KnowledgeNode['sourcePlatform'], - sourceId: row['source_id'] as string | undefined, - sourceUrl: row['source_url'] as string | undefined, - sourceChain: safeJsonParse(row['source_chain'] as string, []), - gitContext: row['git_context'] ? safeJsonParse(row['git_context'] as string, undefined) : undefined, - confidence: row['confidence'] as number, - isContradicted: Boolean(row['is_contradicted']), - contradictionIds: safeJsonParse(row['contradiction_ids'] as string, []), - people: safeJsonParse(row['people'] as string, []), - concepts: safeJsonParse(row['concepts'] as string, []), - events: safeJsonParse(row['events'] as string, []), - tags: safeJsonParse(row['tags'] as string, []), - }; - } - - private rowToPerson(row: Record): PersonNode { - // Use safe JSON parsing with fallbacks to prevent crashes from corrupted data - return { - id: row['id'] as string, - name: row['name'] as string, - aliases: safeJsonParse(row['aliases'] as string, []), - howWeMet: row['how_we_met'] as string | undefined, - relationshipType: row['relationship_type'] as string | undefined, - organization: row['organization'] as string | undefined, - role: row['role'] as string | undefined, - location: row['location'] as string | undefined, - email: row['email'] as string | undefined, - phone: row['phone'] as string | undefined, - socialLinks: safeJsonParse>(row['social_links'] as string, {}), - lastContactAt: row['last_contact_at'] ? new Date(row['last_contact_at'] as string) : undefined, - contactFrequency: row['contact_frequency'] as number, - preferredChannel: row['preferred_channel'] as string | undefined, - sharedTopics: safeJsonParse(row['shared_topics'] as string, []), - sharedProjects: safeJsonParse(row['shared_projects'] as string, []), - notes: row['notes'] as string | undefined, - relationshipHealth: row['relationship_health'] as number, - createdAt: new Date(row['created_at'] as string), - updatedAt: new Date(row['updated_at'] as string), - }; - } - - close(): void { - try { - // Checkpoint WAL before closing - this.db.pragma('wal_checkpoint(TRUNCATE)'); - this.db.close(); - } catch { - // Ignore close errors - } - } -} diff --git a/packages/core/src/core/embeddings.ts b/packages/core/src/core/embeddings.ts deleted file mode 100644 index 7219151..0000000 --- a/packages/core/src/core/embeddings.ts +++ /dev/null @@ -1,788 +0,0 @@ -/** - * Embeddings Service - Semantic Understanding for Vestige - * - * Provides vector embeddings for knowledge nodes using Ollama. - * Embeddings enable semantic similarity search and connection discovery. - * - * Features: - * - Ollama integration with nomic-embed-text model (768-dim, fast, high quality) - * - Graceful fallback to TF-IDF when Ollama unavailable - * - Availability caching to reduce connection overhead - * - Batch embedding support for efficiency - * - Utility functions for similarity search - */ - -import { Ollama } from 'ollama'; - -// ============================================================================ -// CONFIGURATION -// ============================================================================ - -/** - * Ollama API endpoint. Defaults to local installation. - */ -const OLLAMA_HOST = process.env['OLLAMA_HOST'] || 'http://localhost:11434'; - -/** - * Embedding model to use. nomic-embed-text provides: - * - 768 dimensions - * - Fast inference - * - High quality embeddings for semantic search - * - 8192 token context window - */ -const EMBEDDING_MODEL = process.env['VESTIGE_EMBEDDING_MODEL'] || 'nomic-embed-text'; - -/** - * Maximum characters to embed. nomic-embed-text supports ~8192 tokens, - * but we truncate to 8000 chars for safety margin. - */ -const MAX_TEXT_LENGTH = 8000; - -/** - * Cache duration for availability check (5 minutes in ms) - */ -const AVAILABILITY_CACHE_TTL = 5 * 60 * 1000; - -/** - * Default request timeout in milliseconds - */ -const DEFAULT_TIMEOUT = 30000; - -// ============================================================================ -// INTERFACES -// ============================================================================ - -/** - * Service interface for generating and comparing text embeddings. - * Provides semantic similarity capabilities for knowledge retrieval. - */ -export interface EmbeddingService { - /** - * Generate an embedding vector for the given text. - * @param text - The text to embed - * @returns A promise resolving to a numeric vector - */ - generateEmbedding(text: string): Promise; - - /** - * Generate embeddings for multiple texts in a single batch. - * More efficient than calling generateEmbedding multiple times. - * @param texts - Array of texts to embed - * @returns A promise resolving to an array of embedding vectors - */ - batchEmbeddings(texts: string[]): Promise; - - /** - * Calculate similarity between two embedding vectors. - * @param embA - First embedding vector - * @param embB - Second embedding vector - * @returns Similarity score between 0 and 1 - */ - getSimilarity(embA: number[], embB: number[]): number; - - /** - * Check if the embedding service is available and ready. - * @returns A promise resolving to true if the service is available - */ - isAvailable(): Promise; -} - -/** - * Configuration options for embedding services. - */ -export interface EmbeddingServiceConfig { - /** Ollama host URL (default: http://localhost:11434) */ - host?: string; - /** Embedding model to use (default: nomic-embed-text) */ - model?: string; - /** Request timeout in milliseconds (default: 30000) */ - timeout?: number; -} - -/** - * Result from embedding generation with metadata. - */ -export interface EmbeddingResult { - embedding: number[]; - model: string; - dimension: number; -} - -// ============================================================================ -// COSINE SIMILARITY -// ============================================================================ - -/** - * Calculate cosine similarity between two vectors. - * Returns a value between -1 and 1, where: - * - 1 means identical direction - * - 0 means orthogonal (unrelated) - * - -1 means opposite direction - * - * @param a - First vector - * @param b - Second vector - * @returns Cosine similarity score - * @throws Error if vectors have different lengths or are empty - */ -export function cosineSimilarity(a: number[], b: number[]): number { - if (a.length === 0 || b.length === 0) { - throw new Error('Cannot compute cosine similarity of empty vectors'); - } - - if (a.length !== b.length) { - throw new Error( - `Vector dimension mismatch: ${a.length} vs ${b.length}` - ); - } - - let dotProduct = 0; - let normA = 0; - let normB = 0; - - for (let i = 0; i < a.length; i++) { - const aVal = a[i]!; - const bVal = b[i]!; - dotProduct += aVal * bVal; - normA += aVal * aVal; - normB += bVal * bVal; - } - - const magnitude = Math.sqrt(normA) * Math.sqrt(normB); - - if (magnitude === 0) { - return 0; - } - - return dotProduct / magnitude; -} - -/** - * Normalize cosine similarity from [-1, 1] to [0, 1] range. - * Useful when you need a percentage-like similarity score. - * - * @param similarity - Cosine similarity value - * @returns Normalized similarity between 0 and 1 - */ -export function normalizedSimilarity(similarity: number): number { - return (similarity + 1) / 2; -} - -/** - * Calculate Euclidean distance between two vectors. - * - * @param a - First vector - * @param b - Second vector - * @returns Euclidean distance (lower = more similar) - */ -export function euclideanDistance(a: number[], b: number[]): number { - if (a.length !== b.length) { - throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`); - } - - let sum = 0; - for (let i = 0; i < a.length; i++) { - const diff = a[i]! - b[i]!; - sum += diff * diff; - } - - return Math.sqrt(sum); -} - -// ============================================================================ -// OLLAMA EMBEDDING SERVICE -// ============================================================================ - -/** - * Production embedding service using Ollama with nomic-embed-text model. - * Provides high-quality semantic embeddings for knowledge retrieval. - * - * Features: - * - Automatic text truncation for long inputs - * - Availability caching to reduce connection overhead - * - Graceful error handling with informative messages - * - Batch embedding support for efficiency - */ -export class OllamaEmbeddingService implements EmbeddingService { - private client: Ollama; - private availabilityCache: { available: boolean; timestamp: number } | null = null; - private readonly model: string; - private readonly timeout: number; - - constructor(config: EmbeddingServiceConfig = {}) { - const { - host = OLLAMA_HOST, - model = EMBEDDING_MODEL, - timeout = DEFAULT_TIMEOUT, - } = config; - - this.client = new Ollama({ host }); - this.model = model; - this.timeout = timeout; - } - - /** - * Check if Ollama is running and the embedding model is available. - * Results are cached for 5 minutes to reduce overhead. - */ - async isAvailable(): Promise { - // Check cache first - if ( - this.availabilityCache && - Date.now() - this.availabilityCache.timestamp < AVAILABILITY_CACHE_TTL - ) { - return this.availabilityCache.available; - } - - try { - // Try to list models to verify connection with timeout - const response = await Promise.race([ - this.client.list(), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), this.timeout) - ), - ]); - - const modelNames = response.models.map((m) => m.name); - - // Check if our model is available (handle both "model" and "model:latest" formats) - const modelBase = this.model.split(':')[0]; - const available = modelNames.some( - (name) => name === this.model || - name.startsWith(`${this.model}:`) || - name.split(':')[0] === modelBase - ); - - if (!available) { - console.warn( - `Ollama is running but model '${this.model}' not found. ` + - `Available models: ${modelNames.join(', ') || 'none'}. ` + - `Run 'ollama pull ${this.model}' to install.` - ); - } - - this.availabilityCache = { available, timestamp: Date.now() }; - return available; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn(`Ollama not available: ${message}`); - this.availabilityCache = { available: false, timestamp: Date.now() }; - return false; - } - } - - /** - * Truncate text to fit within the model's context window. - */ - private truncateText(text: string): string { - if (text.length <= MAX_TEXT_LENGTH) { - return text; - } - console.warn( - `Text truncated from ${text.length} to ${MAX_TEXT_LENGTH} characters` - ); - return text.slice(0, MAX_TEXT_LENGTH); - } - - /** - * Generate an embedding for the given text. - */ - async generateEmbedding(text: string): Promise { - if (!text || text.trim().length === 0) { - throw new Error('Cannot generate embedding for empty text'); - } - - const truncatedText = this.truncateText(text.trim()); - - try { - const response = await Promise.race([ - this.client.embed({ - model: this.model, - input: truncatedText, - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Embedding timeout')), this.timeout) - ), - ]); - - // Response contains array of embeddings, we want the first one - if (!response.embeddings || response.embeddings.length === 0) { - throw new Error('No embeddings returned from Ollama'); - } - - const embedding = response.embeddings[0]; - if (!embedding) { - throw new Error('No embedding returned from Ollama'); - } - - return embedding; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to generate embedding: ${message}`); - } - } - - /** - * Generate embeddings for multiple texts in a batch. - * More efficient than individual calls for bulk operations. - */ - async batchEmbeddings(texts: string[]): Promise { - if (texts.length === 0) { - return []; - } - - // Filter and truncate texts - const validTexts = texts - .filter((t) => t && t.trim().length > 0) - .map((t) => this.truncateText(t.trim())); - - if (validTexts.length === 0) { - return []; - } - - try { - const response = await Promise.race([ - this.client.embed({ - model: this.model, - input: validTexts, - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Batch embedding timeout')), this.timeout * 2) - ), - ]); - - if (!response.embeddings || response.embeddings.length !== validTexts.length) { - throw new Error( - `Expected ${validTexts.length} embeddings, got ${response.embeddings?.length ?? 0}` - ); - } - - return response.embeddings; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to generate batch embeddings: ${message}`); - } - } - - /** - * Calculate similarity between two embedding vectors using cosine similarity. - */ - getSimilarity(embA: number[], embB: number[]): number { - return cosineSimilarity(embA, embB); - } - - /** - * Get the model being used. - */ - getModel(): string { - return this.model; - } - - /** - * Clear the availability cache, forcing a fresh check on next call. - */ - clearCache(): void { - this.availabilityCache = null; - } -} - -// ============================================================================ -// FALLBACK EMBEDDING SERVICE -// ============================================================================ - -/** - * Default vocabulary size for fallback TF-IDF style embeddings. - */ -const DEFAULT_VOCAB_SIZE = 512; - -/** - * Fallback embedding service using TF-IDF style word frequency vectors. - * Used when Ollama is not available. Provides basic keyword-based - * similarity that works offline with no dependencies. - * - * Limitations compared to Ollama: - * - No semantic understanding (only keyword matching) - * - Fixed vocabulary may miss domain-specific terms - * - Lower quality similarity scores - */ -export class FallbackEmbeddingService implements EmbeddingService { - private readonly dimensions: number; - private readonly vocabulary: Map; - private documentFrequency: Map; - private documentCount: number; - - constructor(vocabSize: number = DEFAULT_VOCAB_SIZE) { - this.dimensions = vocabSize; - this.vocabulary = new Map(); - this.documentFrequency = new Map(); - this.documentCount = 0; - } - - /** - * Fallback service is always available (runs locally with no dependencies). - */ - async isAvailable(): Promise { - return true; - } - - /** - * Tokenize text into normalized words. - */ - private tokenize(text: string): string[] { - return text - .toLowerCase() - .replace(/[^\w\s]/g, ' ') - .split(/\s+/) - .filter((word) => word.length > 2 && word.length < 30); - } - - /** - * Get or assign a vocabulary index for a word. - * Uses hash-based assignment for consistent but bounded vocabulary. - */ - private getWordIndex(word: string): number { - if (this.vocabulary.has(word)) { - return this.vocabulary.get(word)!; - } - - // Simple hash function for consistent word-to-index mapping - let hash = 0; - for (let i = 0; i < word.length; i++) { - const char = word.charCodeAt(i); - hash = ((hash << 5) - hash + char) | 0; - } - const index = Math.abs(hash) % this.dimensions; - this.vocabulary.set(word, index); - return index; - } - - /** - * Generate a TF-IDF style embedding vector. - * Uses term frequency weighted by inverse document frequency approximation. - */ - async generateEmbedding(text: string): Promise { - if (!text || text.trim().length === 0) { - throw new Error('Cannot generate embedding for empty text'); - } - - const tokens = this.tokenize(text); - if (tokens.length === 0) { - // Return zero vector for text with no valid tokens - return new Array(this.dimensions).fill(0); - } - - // Calculate term frequency - const termFreq = new Map(); - for (const token of tokens) { - termFreq.set(token, (termFreq.get(token) || 0) + 1); - } - - // Update document frequency for IDF - this.documentCount++; - const seenWords = new Set(); - for (const token of tokens) { - if (!seenWords.has(token)) { - this.documentFrequency.set( - token, - (this.documentFrequency.get(token) || 0) + 1 - ); - seenWords.add(token); - } - } - - // Build embedding vector - const embedding = new Array(this.dimensions).fill(0); - const maxFreq = Math.max(...termFreq.values()); - - for (const [word, freq] of termFreq) { - const index = this.getWordIndex(word); - - // TF: normalized term frequency (prevents bias towards long documents) - const tf = freq / maxFreq; - - // IDF: inverse document frequency (common words get lower weight) - const df = this.documentFrequency.get(word) || 1; - const idf = Math.log((this.documentCount + 1) / (df + 1)) + 1; - - // TF-IDF score (may have collisions, add to handle) - embedding[index] += tf * idf; - } - - // L2 normalize the vector - const norm = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0)); - if (norm > 0) { - for (let i = 0; i < embedding.length; i++) { - embedding[i] /= norm; - } - } - - return embedding; - } - - /** - * Generate embeddings for multiple texts. - */ - async batchEmbeddings(texts: string[]): Promise { - const embeddings: number[][] = []; - for (const text of texts) { - if (text && text.trim().length > 0) { - embeddings.push(await this.generateEmbedding(text)); - } - } - return embeddings; - } - - /** - * Calculate similarity between two embedding vectors. - */ - getSimilarity(embA: number[], embB: number[]): number { - return cosineSimilarity(embA, embB); - } - - /** - * Reset the document frequency statistics. - * Useful when starting fresh with a new corpus. - */ - reset(): void { - this.vocabulary.clear(); - this.documentFrequency.clear(); - this.documentCount = 0; - } - - /** - * Get the dimensionality of embeddings. - */ - getDimensions(): number { - return this.dimensions; - } -} - -// ============================================================================ -// EMBEDDING CACHE -// ============================================================================ - -/** - * Simple in-memory cache for embeddings. - * Reduces redundant API calls during REM cycles. - */ -export class EmbeddingCache { - private cache: Map = new Map(); - private maxSize: number; - private ttlMs: number; - - constructor(maxSize: number = 1000, ttlMinutes: number = 60) { - this.maxSize = maxSize; - this.ttlMs = ttlMinutes * 60 * 1000; - } - - /** - * Get a cached embedding by node ID. - */ - get(nodeId: string): number[] | null { - const entry = this.cache.get(nodeId); - if (!entry) return null; - - // Check if expired - if (Date.now() - entry.timestamp > this.ttlMs) { - this.cache.delete(nodeId); - return null; - } - - return entry.embedding; - } - - /** - * Cache an embedding for a node ID. - */ - set(nodeId: string, embedding: number[]): void { - // Evict oldest if at capacity - if (this.cache.size >= this.maxSize) { - const oldestKey = this.cache.keys().next().value; - if (oldestKey) { - this.cache.delete(oldestKey); - } - } - - this.cache.set(nodeId, { - embedding, - timestamp: Date.now(), - }); - } - - /** - * Check if a node ID has a cached embedding. - */ - has(nodeId: string): boolean { - return this.get(nodeId) !== null; - } - - /** - * Clear all cached embeddings. - */ - clear(): void { - this.cache.clear(); - } - - /** - * Get the number of cached embeddings. - */ - size(): number { - return this.cache.size; - } -} - -// ============================================================================ -// FACTORY FUNCTIONS -// ============================================================================ - -let defaultService: EmbeddingService | null = null; - -/** - * Get the default embedding service (singleton). - * Uses cached instance for efficiency. - */ -export function getEmbeddingService(config?: EmbeddingServiceConfig): EmbeddingService { - if (!defaultService) { - defaultService = new OllamaEmbeddingService(config); - } - return defaultService; -} - -/** - * Create an embedding service with automatic fallback. - * - * Attempts to use Ollama with nomic-embed-text for high-quality semantic - * embeddings. Falls back to TF-IDF based keyword similarity if Ollama - * is not available. - * - * @param config - Optional configuration for the Ollama service - * @returns A promise resolving to an EmbeddingService instance - * - * @example - * ```typescript - * const embeddings = await createEmbeddingService(); - * - * const vec1 = await embeddings.generateEmbedding("TypeScript is great"); - * const vec2 = await embeddings.generateEmbedding("JavaScript is popular"); - * - * const similarity = embeddings.getSimilarity(vec1, vec2); - * console.log(`Similarity: ${similarity}`); - * ``` - */ -export async function createEmbeddingService( - config?: EmbeddingServiceConfig -): Promise { - const ollama = new OllamaEmbeddingService(config); - - if (await ollama.isAvailable()) { - console.log(`Using Ollama embedding service with model: ${config?.model || EMBEDDING_MODEL}`); - return ollama; - } - - console.warn( - 'Ollama not available, using fallback keyword similarity. ' + - 'For better results, install Ollama and run: ollama pull nomic-embed-text' - ); - return new FallbackEmbeddingService(); -} - -// ============================================================================ -// UTILITY FUNCTIONS -// ============================================================================ - -/** - * Find the top K most similar items to a query embedding. - * - * @param queryEmbedding - The embedding to search for - * @param candidates - Array of items with embeddings - * @param k - Number of results to return - * @returns Top K items sorted by similarity (highest first) - * - * @example - * ```typescript - * const results = findTopK(queryVec, documents, 10); - * results.forEach(({ item, similarity }) => { - * console.log(`${item.title}: ${similarity.toFixed(3)}`); - * }); - * ``` - */ -export function findTopK( - queryEmbedding: number[], - candidates: T[], - k: number -): Array { - const scored = candidates.map((item) => ({ - ...item, - similarity: cosineSimilarity(queryEmbedding, item.embedding), - })); - - scored.sort((a, b) => b.similarity - a.similarity); - - return scored.slice(0, k); -} - -/** - * Filter items by minimum similarity threshold. - * - * @param queryEmbedding - The embedding to search for - * @param candidates - Array of items with embeddings - * @param minSimilarity - Minimum similarity score (-1 to 1) - * @returns Items with similarity >= minSimilarity, sorted by similarity - * - * @example - * ```typescript - * const relevant = filterBySimilarity(queryVec, documents, 0.7); - * console.log(`Found ${relevant.length} relevant documents`); - * ``` - */ -export function filterBySimilarity( - queryEmbedding: number[], - candidates: T[], - minSimilarity: number -): Array { - const scored = candidates - .map((item) => ({ - ...item, - similarity: cosineSimilarity(queryEmbedding, item.embedding), - })) - .filter((item) => item.similarity >= minSimilarity); - - scored.sort((a, b) => b.similarity - a.similarity); - - return scored; -} - -/** - * Compute average embedding from multiple vectors. - * Useful for combining multiple documents into a single representation. - * - * @param embeddings - Array of embedding vectors - * @returns Average embedding vector - */ -export function averageEmbedding(embeddings: number[][]): number[] { - if (embeddings.length === 0) { - throw new Error('Cannot compute average of empty embedding array'); - } - - const firstEmbedding = embeddings[0]; - if (!firstEmbedding) { - throw new Error('Cannot compute average of empty embedding array'); - } - - const dimensions = firstEmbedding.length; - const result = new Array(dimensions).fill(0); - - for (const embedding of embeddings) { - if (embedding.length !== dimensions) { - throw new Error('All embeddings must have the same dimensions'); - } - for (let i = 0; i < dimensions; i++) { - result[i]! += embedding[i]!; - } - } - - for (let i = 0; i < dimensions; i++) { - result[i]! /= embeddings.length; - } - - return result; -} diff --git a/packages/core/src/core/errors.ts b/packages/core/src/core/errors.ts deleted file mode 100644 index f104255..0000000 --- a/packages/core/src/core/errors.ts +++ /dev/null @@ -1,462 +0,0 @@ -/** - * Vestige Error Types - * - * A comprehensive hierarchy of errors for proper error handling and reporting. - * Includes type guards, utilities, and a Result type for functional error handling. - */ - -// ============================================================================= -// Error Sanitization -// ============================================================================= - -/** - * Sanitize error messages to prevent information leakage - */ -export function sanitizeErrorMessage(message: string): string { - let sanitized = message; - // Remove file paths - sanitized = sanitized.replace(/\/[^\s]+/g, '[PATH]'); - // Remove SQL keywords - sanitized = sanitized.replace(/SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER/gi, '[SQL]'); - // Redact credentials - sanitized = sanitized.replace( - /\b(password|secret|key|token|auth)\s*[=:]\s*\S+/gi, - '[REDACTED]' - ); - return sanitized; -} - -// ============================================================================= -// Base Error Class -// ============================================================================= - -/** - * Base error class for all Vestige errors - */ -export class VestigeError extends Error { - constructor( - message: string, - public readonly code: string, - public readonly statusCode: number = 500, - public readonly details?: Record - ) { - super(message); - this.name = 'VestigeError'; - Error.captureStackTrace(this, this.constructor); - } - - toJSON(): { - name: string; - code: string; - message: string; - statusCode: number; - details?: Record; - } { - const result: { - name: string; - code: string; - message: string; - statusCode: number; - details?: Record; - } = { - name: this.name, - code: this.code, - message: this.message, - statusCode: this.statusCode, - }; - if (this.details !== undefined) { - result.details = this.details; - } - return result; - } -} - -// ============================================================================= -// Specific Error Types -// ============================================================================= - -/** - * Validation errors (400) - */ -export class ValidationError extends VestigeError { - constructor(message: string, details?: Record) { - super(message, 'VALIDATION_ERROR', 400, details); - this.name = 'ValidationError'; - } -} - -/** - * Resource not found (404) - */ -export class NotFoundError extends VestigeError { - constructor(resource: string, id?: string) { - super( - id ? `${resource} not found: ${id}` : `${resource} not found`, - 'NOT_FOUND', - 404, - { resource, id } - ); - this.name = 'NotFoundError'; - } -} - -/** - * Conflict errors (409) - */ -export class ConflictError extends VestigeError { - constructor(message: string, details?: Record) { - super(message, 'CONFLICT', 409, details); - this.name = 'ConflictError'; - } -} - -/** - * Database operation errors (500) - */ -export class DatabaseError extends VestigeError { - constructor(message: string, cause?: unknown) { - super(sanitizeErrorMessage(message), 'DATABASE_ERROR', 500, { - cause: String(cause), - }); - this.name = 'DatabaseError'; - } -} - -/** - * Security-related errors (403) - */ -export class SecurityError extends VestigeError { - constructor(message: string, details?: Record) { - super(message, 'SECURITY_ERROR', 403, details); - this.name = 'SecurityError'; - } -} - -/** - * Configuration errors (500) - */ -export class ConfigurationError extends VestigeError { - constructor(message: string, details?: Record) { - super(message, 'CONFIGURATION_ERROR', 500, details); - this.name = 'ConfigurationError'; - } -} - -/** - * Timeout errors (408) - */ -export class TimeoutError extends VestigeError { - constructor(operation: string, timeoutMs: number) { - super(`Operation timed out: ${operation}`, 'TIMEOUT', 408, { - operation, - timeoutMs, - }); - this.name = 'TimeoutError'; - } -} - -/** - * Embedding service errors - */ -export class EmbeddingError extends VestigeError { - constructor(message: string, cause?: unknown) { - super(message, 'EMBEDDING_ERROR', 500, { cause: String(cause) }); - this.name = 'EmbeddingError'; - } -} - -/** - * Concurrency/locking errors (409) - */ -export class ConcurrencyError extends VestigeError { - constructor(message: string = 'Operation failed due to concurrent access') { - super(message, 'CONCURRENCY_ERROR', 409); - this.name = 'ConcurrencyError'; - } -} - -/** - * Rate limit errors (429) - */ -export class RateLimitError extends VestigeError { - constructor(message: string, retryAfterMs?: number) { - super(message, 'RATE_LIMIT', 429, { retryAfterMs }); - this.name = 'RateLimitError'; - } -} - -/** - * Authentication errors (401) - */ -export class AuthenticationError extends VestigeError { - constructor(message: string = 'Authentication required') { - super(message, 'AUTHENTICATION_ERROR', 401); - this.name = 'AuthenticationError'; - } -} - -// ============================================================================= -// Error Handling Utilities -// ============================================================================= - -/** - * Type guard for VestigeError - */ -export function isVestigeError(error: unknown): error is VestigeError { - return error instanceof VestigeError; -} - -/** - * Convert unknown error to VestigeError - */ -export function toVestigeError(error: unknown): VestigeError { - if (isVestigeError(error)) { - return error; - } - - if (error instanceof Error) { - return new VestigeError( - sanitizeErrorMessage(error.message), - 'UNKNOWN_ERROR', - 500, - { originalName: error.name } - ); - } - - if (typeof error === 'string') { - return new VestigeError(sanitizeErrorMessage(error), 'UNKNOWN_ERROR', 500); - } - - return new VestigeError('An unknown error occurred', 'UNKNOWN_ERROR', 500, { - errorType: typeof error, - }); -} - -/** - * Wrap function to catch and transform errors - */ -export function wrapError Promise>( - fn: T, - errorTransform?: (error: unknown) => VestigeError -): T { - const wrapped = async (...args: Parameters): Promise> => { - try { - return (await fn(...args)) as ReturnType; - } catch (error) { - if (errorTransform) { - throw errorTransform(error); - } - throw toVestigeError(error); - } - }; - return wrapped as T; -} - -/** - * Execute a function with error transformation - */ -export async function withErrorHandling( - fn: () => Promise, - errorTransform?: (error: unknown) => VestigeError -): Promise { - try { - return await fn(); - } catch (error) { - if (errorTransform) { - throw errorTransform(error); - } - throw toVestigeError(error); - } -} - -/** - * Retry a function with exponential backoff - */ -export async function withRetry( - fn: () => Promise, - options: { - maxRetries?: number; - baseDelayMs?: number; - maxDelayMs?: number; - shouldRetry?: (error: unknown) => boolean; - } = {} -): Promise { - const { - maxRetries = 3, - baseDelayMs = 100, - maxDelayMs = 5000, - shouldRetry = () => true, - } = options; - - let lastError: unknown; - - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - return await fn(); - } catch (error) { - lastError = error; - - if (attempt === maxRetries || !shouldRetry(error)) { - throw toVestigeError(error); - } - - const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - - throw toVestigeError(lastError); -} - -// ============================================================================= -// Result Type (Optional Pattern) -// ============================================================================= - -/** - * Result type for functional error handling - */ -export type Result = - | { success: true; data: T } - | { success: false; error: E }; - -/** - * Create a success result - */ -export function ok(data: T): Result { - return { success: true, data }; -} - -/** - * Create an error result - */ -export function err(error: E): Result { - return { success: false, error }; -} - -/** - * Check if result is success - */ -export function isOk(result: Result): result is { success: true; data: T } { - return result.success; -} - -/** - * Check if result is error - */ -export function isErr(result: Result): result is { success: false; error: E } { - return !result.success; -} - -/** - * Unwrap a result, throwing if it's an error - */ -export function unwrap(result: Result): T { - if (result.success) { - return result.data; - } - throw (result as { success: false; error: E }).error; -} - -/** - * Unwrap a result with a default value - */ -export function unwrapOr(result: Result, defaultValue: T): T { - if (result.success) { - return result.data; - } - return defaultValue; -} - -/** - * Map over a successful result - */ -export function mapResult( - result: Result, - fn: (data: T) => U -): Result { - if (result.success) { - return ok(fn(result.data)); - } - return result as { success: false; error: E }; -} - -/** - * Map over an error result - */ -export function mapError( - result: Result, - fn: (error: E) => F -): Result { - if (!result.success) { - return err(fn((result as { success: false; error: E }).error)); - } - return result as { success: true; data: T }; -} - -/** - * Execute a function and return a Result - */ -export async function tryCatch( - fn: () => Promise -): Promise> { - try { - const data = await fn(); - return ok(data); - } catch (error) { - return err(toVestigeError(error)); - } -} - -/** - * Execute a synchronous function and return a Result - */ -export function tryCatchSync(fn: () => T): Result { - try { - const data = fn(); - return ok(data); - } catch (error) { - return err(toVestigeError(error)); - } -} - -// ============================================================================= -// Error Assertion Helpers -// ============================================================================= - -/** - * Assert a condition, throwing ValidationError if false - */ -export function assertValid( - condition: boolean, - message: string, - details?: Record -): asserts condition { - if (!condition) { - throw new ValidationError(message, details); - } -} - -/** - * Assert a value is not null or undefined - */ -export function assertDefined( - value: T | null | undefined, - resource: string, - id?: string -): asserts value is T { - if (value === null || value === undefined) { - throw new NotFoundError(resource, id); - } -} - -/** - * Assert a value exists, returning it if so - */ -export function requireDefined( - value: T | null | undefined, - resource: string, - id?: string -): T { - assertDefined(value, resource, id); - return value; -} diff --git a/packages/core/src/core/fsrs.ts b/packages/core/src/core/fsrs.ts deleted file mode 100644 index f46d993..0000000 --- a/packages/core/src/core/fsrs.ts +++ /dev/null @@ -1,815 +0,0 @@ -/** - * FSRS-5 (Free Spaced Repetition Scheduler) Algorithm Implementation - * - * Based on the FSRS-5 algorithm by Jarrett Ye - * Paper: https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm - * - * This is a production-ready implementation with full TypeScript types, - * sentiment integration for emotional memory boosting, and comprehensive - * error handling. - */ - -import { z } from 'zod'; - -// ============================================================================ -// FSRS-5 CONSTANTS -// ============================================================================ - -/** - * FSRS-5 default weights (w0 to w18) - * - * These weights are optimized from millions of Anki review records. - * They control: - * - w0-w3: Initial stability for each grade (Again, Hard, Good, Easy) - * - w4-w5: Initial difficulty calculation - * - w6-w7: Short-term stability calculation - * - w8-w10: Stability increase factors after successful recall - * - w11-w14: Difficulty update parameters - * - w15-w16: Forgetting curve (stability after lapse) - * - w17-w18: Short-term scheduling parameters - */ -export const FSRS_WEIGHTS: readonly [ - number, number, number, number, number, - number, number, number, number, number, - number, number, number, number, number, - number, number, number, number -] = [ - 0.40255, 1.18385, 3.173, 15.69105, 7.1949, - 0.5345, 1.4604, 0.0046, 1.54575, 0.1192, - 1.01925, 1.9395, 0.11, 0.29605, 2.2698, - 0.2315, 2.9898, 0.51655, 0.6621 -] as const; - -/** - * FSRS algorithm constants - */ -export const FSRS_CONSTANTS = { - /** Maximum difficulty value */ - MAX_DIFFICULTY: 10, - /** Minimum difficulty value */ - MIN_DIFFICULTY: 1, - /** Minimum stability in days */ - MIN_STABILITY: 0.1, - /** Maximum stability in days (approx 100 years) */ - MAX_STABILITY: 36500, - /** Default desired retention rate */ - DEFAULT_RETENTION: 0.9, - /** Factor for converting retrievability to interval */ - DECAY_FACTOR: 0.9, - /** Small epsilon for numerical stability */ - EPSILON: 1e-10, -} as const; - -// ============================================================================ -// TYPES & SCHEMAS -// ============================================================================ - -/** - * Review grades in FSRS - * - Again (1): Complete failure to recall - * - Hard (2): Recalled with significant difficulty - * - Good (3): Recalled with moderate effort - * - Easy (4): Recalled effortlessly - */ -export const ReviewGradeSchema = z.union([ - z.literal(1), - z.literal(2), - z.literal(3), - z.literal(4), -]); -export type ReviewGrade = z.infer; - -/** - * Named constants for review grades - */ -export const Grade = { - Again: 1, - Hard: 2, - Good: 3, - Easy: 4, -} as const satisfies Record; - -/** - * Learning states for FSRS cards - * - New: Never reviewed - * - Learning: In initial learning phase - * - Review: In long-term review phase - * - Relearning: Lapsed and relearning - */ -export const LearningStateSchema = z.enum([ - 'New', - 'Learning', - 'Review', - 'Relearning', -]); -export type LearningState = z.infer; - -/** - * FSRS card state - represents the memory state of a single item - */ -export const FSRSStateSchema = z.object({ - /** Current difficulty (1-10, higher = harder) */ - difficulty: z.number().min(FSRS_CONSTANTS.MIN_DIFFICULTY).max(FSRS_CONSTANTS.MAX_DIFFICULTY), - /** Current stability in days (higher = more stable memory) */ - stability: z.number().min(FSRS_CONSTANTS.MIN_STABILITY).max(FSRS_CONSTANTS.MAX_STABILITY), - /** Current learning state */ - state: LearningStateSchema, - /** Number of times reviewed */ - reps: z.number().int().min(0), - /** Number of lapses (times "Again" was pressed in Review state) */ - lapses: z.number().int().min(0), - /** Timestamp of last review */ - lastReview: z.date(), - /** Scheduled next review date */ - scheduledDays: z.number().min(0), -}); -export type FSRSState = z.infer; - -/** - * Input type for FSRSState (for creating new states) - */ -export type FSRSStateInput = z.input; - -/** - * Result of a review operation - */ -export const ReviewResultSchema = z.object({ - /** Updated FSRS state */ - state: FSRSStateSchema, - /** Calculated retrievability at time of review */ - retrievability: z.number().min(0).max(1), - /** Next review interval in days */ - interval: z.number().min(0), - /** Whether this was a lapse */ - isLapse: z.boolean(), -}); -export type ReviewResult = z.infer; - -/** - * Type for the 19-element FSRS weights tuple - */ -export type FSRSWeightsTuple = readonly [ - number, number, number, number, number, - number, number, number, number, number, - number, number, number, number, number, - number, number, number, number -]; - -/** - * Zod schema for FSRS weights - */ -const FSRSWeightsSchema = z.array(z.number()).length(19); - -/** - * Configuration for FSRS scheduler - */ -export const FSRSConfigSchema = z.object({ - /** Desired retention rate (0.7-0.99) */ - desiredRetention: z.number().min(0.7).max(0.99).default(0.9), - /** Maximum interval in days */ - maximumInterval: z.number().min(1).max(36500).default(36500), - /** Custom weights (must be exactly 19 values) */ - weights: FSRSWeightsSchema.optional(), - /** Enable sentiment boost for emotional memories */ - enableSentimentBoost: z.boolean().default(true), - /** Maximum sentiment boost multiplier (1.0-3.0) */ - maxSentimentBoost: z.number().min(1).max(3).default(2), -}); - -/** - * Configuration type for FSRS scheduler - */ -export interface FSRSConfig { - /** Desired retention rate (0.7-0.99) */ - desiredRetention?: number; - /** Maximum interval in days */ - maximumInterval?: number; - /** Custom weights (must be exactly 19 values) */ - weights?: readonly number[]; - /** Enable sentiment boost for emotional memories */ - enableSentimentBoost?: boolean; - /** Maximum sentiment boost multiplier (1.0-3.0) */ - maxSentimentBoost?: number; -} - -/** - * Resolved (required) configuration type - */ -export interface ResolvedFSRSConfig { - desiredRetention: number; - maximumInterval: number; - weights: readonly number[] | undefined; - enableSentimentBoost: boolean; - maxSentimentBoost: number; -} - -// ============================================================================ -// CORE FSRS-5 FUNCTIONS -// ============================================================================ - -/** - * Calculate initial difficulty for a new card based on first rating - * - * Formula: D(G) = w[4] - e^(w[5]*(G-1)) + 1 - * - * @param grade - First review grade (1-4) - * @param weights - FSRS weights array - * @returns Initial difficulty (1-10) - */ -export function initialDifficulty( - grade: ReviewGrade, - weights: readonly number[] = FSRS_WEIGHTS -): number { - const w4 = weights[4] ?? FSRS_WEIGHTS[4]; - const w5 = weights[5] ?? FSRS_WEIGHTS[5]; - - // D(G) = w[4] - e^(w[5]*(G-1)) + 1 - const d = w4 - Math.exp(w5 * (grade - 1)) + 1; - - // Clamp to valid range - return clamp(d, FSRS_CONSTANTS.MIN_DIFFICULTY, FSRS_CONSTANTS.MAX_DIFFICULTY); -} - -/** - * Calculate initial stability for a new card based on first rating - * - * Formula: S(G) = w[G-1] (direct lookup from weights 0-3) - * - * Note: FSRS-5 uses the first 4 weights as initial stability values - * for grades 1-4 respectively. - * - * @param grade - First review grade (1-4) - * @param weights - FSRS weights array - * @returns Initial stability in days - */ -export function initialStability( - grade: ReviewGrade, - weights: readonly number[] = FSRS_WEIGHTS -): number { - // FSRS-5: S0(G) = w[G-1] - // Grade is 1-4, so index is 0-3, which is always valid for FSRS_WEIGHTS - const index = grade - 1; - const s = weights[index] ?? FSRS_WEIGHTS[index] ?? FSRS_WEIGHTS[0]; - - // Ensure minimum stability - return Math.max(FSRS_CONSTANTS.MIN_STABILITY, s); -} - -/** - * Calculate retrievability (probability of recall) based on stability and elapsed time - * - * Formula: R = e^(-t/S) where FSRS uses (1 + t/(9*S))^(-1) - * - * This is the power forgetting curve used in FSRS-5. - * - * @param stability - Current stability in days - * @param elapsedDays - Days since last review - * @returns Retrievability (0-1) - */ -export function retrievability(stability: number, elapsedDays: number): number { - if (stability <= 0) { - return 0; - } - - if (elapsedDays <= 0) { - return 1; - } - - // FSRS-5 power forgetting curve: R = (1 + t/(9*S))^(-1) - // This is equivalent to the power law of forgetting - const factor = 9 * stability; - const r = Math.pow(1 + elapsedDays / factor, -1); - - return clamp(r, 0, 1); -} - -/** - * Calculate next difficulty after a review - * - * Formula: D' = w[7] * D + (1 - w[7]) * mean_reversion(D, G) - * where mean_reversion uses a linear combination with the initial difficulty - * - * FSRS-5 mean reversion formula: - * D' = D - w[6] * (G - 3) - * Then: D' = w[7] * D0 + (1 - w[7]) * D' - * - * @param currentD - Current difficulty (1-10) - * @param grade - Review grade (1-4) - * @param weights - FSRS weights array - * @returns New difficulty (1-10) - */ -export function nextDifficulty( - currentD: number, - grade: ReviewGrade, - weights: readonly number[] = FSRS_WEIGHTS -): number { - const w6 = weights[6] ?? FSRS_WEIGHTS[6]; - const w7 = weights[7] ?? FSRS_WEIGHTS[7]; - - // Initial difficulty for mean reversion (what D would be for a "Good" rating) - const d0 = initialDifficulty(Grade.Good, weights); - - // Delta based on grade deviation from "Good" (3) - // Negative grade (Again=1, Hard=2) increases difficulty - // Positive grade (Easy=4) decreases difficulty - const delta = -w6 * (grade - 3); - - // Apply delta to current difficulty - let newD = currentD + delta; - - // Mean reversion: blend towards initial difficulty - newD = w7 * d0 + (1 - w7) * newD; - - return clamp(newD, FSRS_CONSTANTS.MIN_DIFFICULTY, FSRS_CONSTANTS.MAX_DIFFICULTY); -} - -/** - * Calculate next stability after a successful recall - * - * FSRS-5 recall stability formula: - * S'(r) = S * (e^(w[8]) * (11 - D) * S^(-w[9]) * (e^(w[10]*(1-R)) - 1) * hardPenalty * easyBonus + 1) - * - * This is the full FSRS-5 stability increase formula that accounts for: - * - Current stability (S) - * - Difficulty (D) - * - Retrievability at time of review (R) - * - Hard penalty for grade 2 - * - Easy bonus for grade 4 - * - * @param currentS - Current stability in days - * @param difficulty - Current difficulty (1-10) - * @param retrievabilityR - Retrievability at time of review (0-1) - * @param grade - Review grade (2, 3, or 4 - not 1, which is a lapse) - * @param weights - FSRS weights array - * @returns New stability in days - */ -export function nextRecallStability( - currentS: number, - difficulty: number, - retrievabilityR: number, - grade: ReviewGrade, - weights: readonly number[] = FSRS_WEIGHTS -): number { - if (grade === Grade.Again) { - // Lapse - use forget stability instead - return nextForgetStability(difficulty, currentS, retrievabilityR, weights); - } - - const w8 = weights[8] ?? FSRS_WEIGHTS[8]; - const w9 = weights[9] ?? FSRS_WEIGHTS[9]; - const w10 = weights[10] ?? FSRS_WEIGHTS[10]; - const w15 = weights[15] ?? FSRS_WEIGHTS[15]; - const w16 = weights[16] ?? FSRS_WEIGHTS[16]; - - // Hard penalty (grade = 2) - const hardPenalty = grade === Grade.Hard ? w15 : 1; - - // Easy bonus (grade = 4) - const easyBonus = grade === Grade.Easy ? w16 : 1; - - // FSRS-5 recall stability formula - // S'(r) = S * (e^(w8) * (11 - D) * S^(-w9) * (e^(w10*(1-R)) - 1) * hardPenalty * easyBonus + 1) - const factor = - Math.exp(w8) * - (11 - difficulty) * - Math.pow(currentS, -w9) * - (Math.exp(w10 * (1 - retrievabilityR)) - 1) * - hardPenalty * - easyBonus + - 1; - - const newS = currentS * factor; - - return clamp(newS, FSRS_CONSTANTS.MIN_STABILITY, FSRS_CONSTANTS.MAX_STABILITY); -} - -/** - * Calculate stability after a lapse (forgotten/Again rating) - * - * FSRS-5 forget stability formula: - * S'(f) = w[11] * D^(-w[12]) * ((S+1)^w[13] - 1) * e^(w[14]*(1-R)) - * - * This calculates the new stability after forgetting, which is typically - * much lower than the previous stability but not zero (some memory trace remains). - * - * @param difficulty - Current difficulty (1-10) - * @param currentS - Current stability before lapse - * @param retrievabilityR - Retrievability at time of review - * @param weights - FSRS weights array - * @returns New stability after lapse in days - */ -export function nextForgetStability( - difficulty: number, - currentS: number, - retrievabilityR: number = 0.5, - weights: readonly number[] = FSRS_WEIGHTS -): number { - const w11 = weights[11] ?? FSRS_WEIGHTS[11]; - const w12 = weights[12] ?? FSRS_WEIGHTS[12]; - const w13 = weights[13] ?? FSRS_WEIGHTS[13]; - const w14 = weights[14] ?? FSRS_WEIGHTS[14]; - - // S'(f) = w11 * D^(-w12) * ((S+1)^w13 - 1) * e^(w14*(1-R)) - const newS = - w11 * - Math.pow(difficulty, -w12) * - (Math.pow(currentS + 1, w13) - 1) * - Math.exp(w14 * (1 - retrievabilityR)); - - return clamp(newS, FSRS_CONSTANTS.MIN_STABILITY, FSRS_CONSTANTS.MAX_STABILITY); -} - -/** - * Calculate next review interval based on stability and desired retention - * - * Formula: I = S * ln(R) / ln(0.9) where we solve for t when R = desired_retention - * Using the power forgetting curve: I = 9 * S * (1/R - 1) - * - * @param stability - Current stability in days - * @param desiredRetention - Target retention rate (default 0.9) - * @returns Interval in days until next review - */ -export function nextInterval( - stability: number, - desiredRetention: number = FSRS_CONSTANTS.DEFAULT_RETENTION -): number { - if (stability <= 0) { - return 0; - } - - if (desiredRetention >= 1) { - return 0; // If we want 100% retention, review immediately - } - - if (desiredRetention <= 0) { - return FSRS_CONSTANTS.MAX_STABILITY; // If we don't care about retention - } - - // Solve for t in: R = (1 + t/(9*S))^(-1) - // t = 9 * S * (R^(-1) - 1) - const interval = 9 * stability * (Math.pow(desiredRetention, -1) - 1); - - return Math.max(0, Math.round(interval)); -} - -/** - * Apply sentiment boost to stability - * - * Emotional memories are encoded more strongly and decay more slowly. - * This function applies a multiplier to stability based on sentiment intensity. - * - * @param stability - Base stability in days - * @param sentimentIntensity - Sentiment intensity (0-1, where 1 = highly emotional) - * @param maxBoost - Maximum boost multiplier (default 2.0) - * @returns Boosted stability in days - */ -export function applySentimentBoost( - stability: number, - sentimentIntensity: number, - maxBoost: number = 2.0 -): number { - // Validate inputs - const clampedSentiment = clamp(sentimentIntensity, 0, 1); - const clampedMaxBoost = clamp(maxBoost, 1, 3); - - // Linear interpolation: boost = 1 + (maxBoost - 1) * sentimentIntensity - const boost = 1 + (clampedMaxBoost - 1) * clampedSentiment; - - return stability * boost; -} - -// ============================================================================ -// FSRS SCHEDULER CLASS -// ============================================================================ - -/** - * FSRSScheduler - Main class for FSRS-5 spaced repetition scheduling - * - * Usage: - * ```typescript - * const scheduler = new FSRSScheduler(); - * - * // Create initial state for a new card - * const state = scheduler.newCard(); - * - * // Process a review - * const result = scheduler.review(state, Grade.Good, 1); - * - * // Get the next review date - * const nextReview = new Date(); - * nextReview.setDate(nextReview.getDate() + result.interval); - * ``` - */ -export class FSRSScheduler { - private readonly config: ResolvedFSRSConfig; - private readonly weights: readonly number[]; - - /** - * Create a new FSRS scheduler - * - * @param config - Optional configuration overrides - */ - constructor(config: FSRSConfig = {}) { - const parsed = FSRSConfigSchema.parse({ - desiredRetention: config.desiredRetention ?? 0.9, - maximumInterval: config.maximumInterval ?? 36500, - weights: config.weights ? [...config.weights] : undefined, - enableSentimentBoost: config.enableSentimentBoost ?? true, - maxSentimentBoost: config.maxSentimentBoost ?? 2, - }); - - // Extract weights as a readonly number array (or undefined) - const parsedWeights: readonly number[] | undefined = parsed.weights - ? [...parsed.weights] - : undefined; - - this.config = { - desiredRetention: parsed.desiredRetention ?? 0.9, - maximumInterval: parsed.maximumInterval ?? 36500, - weights: parsedWeights, - enableSentimentBoost: parsed.enableSentimentBoost ?? true, - maxSentimentBoost: parsed.maxSentimentBoost ?? 2, - }; - - this.weights = this.config.weights ?? FSRS_WEIGHTS; - } - - /** - * Create initial state for a new card - * - * @returns Initial FSRS state - */ - newCard(): FSRSState { - return { - difficulty: initialDifficulty(Grade.Good, this.weights), - stability: initialStability(Grade.Good, this.weights), - state: 'New', - reps: 0, - lapses: 0, - lastReview: new Date(), - scheduledDays: 0, - }; - } - - /** - * Process a review and calculate next state - * - * @param currentState - Current FSRS state - * @param grade - Review grade (1-4) - * @param elapsedDays - Days since last review (0 for first review) - * @param sentimentBoost - Optional sentiment intensity for emotional memories (0-1) - * @returns Review result with updated state and next interval - */ - review( - currentState: FSRSState, - grade: ReviewGrade, - elapsedDays: number = 0, - sentimentBoost?: number - ): ReviewResult { - // Validate grade - const validatedGrade = ReviewGradeSchema.parse(grade); - - // Calculate retrievability at time of review - const r = currentState.state === 'New' - ? 1 - : retrievability(currentState.stability, Math.max(0, elapsedDays)); - - let newState: FSRSState; - let isLapse = false; - - if (currentState.state === 'New') { - // First review - initialize based on grade - newState = this.handleFirstReview(currentState, validatedGrade); - } else if (validatedGrade === Grade.Again) { - // Lapse - memory failed - isLapse = currentState.state === 'Review' || currentState.state === 'Relearning'; - newState = this.handleLapse(currentState, r); - } else { - // Successful recall - newState = this.handleRecall(currentState, validatedGrade, r); - } - - // Apply sentiment boost if enabled and provided - if ( - this.config.enableSentimentBoost && - sentimentBoost !== undefined && - sentimentBoost > 0 - ) { - newState.stability = applySentimentBoost( - newState.stability, - sentimentBoost, - this.config.maxSentimentBoost - ); - } - - // Calculate next interval - const interval = Math.min( - nextInterval(newState.stability, this.config.desiredRetention), - this.config.maximumInterval - ); - - newState.scheduledDays = interval; - newState.lastReview = new Date(); - - return { - state: newState, - retrievability: r, - interval, - isLapse, - }; - } - - /** - * Handle first review of a new card - */ - private handleFirstReview(currentState: FSRSState, grade: ReviewGrade): FSRSState { - const d = initialDifficulty(grade, this.weights); - const s = initialStability(grade, this.weights); - - return { - ...currentState, - difficulty: d, - stability: s, - state: grade === Grade.Again ? 'Learning' : grade === Grade.Hard ? 'Learning' : 'Review', - reps: 1, - lapses: grade === Grade.Again ? 1 : 0, - }; - } - - /** - * Handle a lapse (Again rating) - */ - private handleLapse(currentState: FSRSState, retrievabilityR: number): FSRSState { - const newS = nextForgetStability( - currentState.difficulty, - currentState.stability, - retrievabilityR, - this.weights - ); - - // Difficulty increases on lapse - const newD = nextDifficulty(currentState.difficulty, Grade.Again, this.weights); - - return { - ...currentState, - difficulty: newD, - stability: newS, - state: 'Relearning', - reps: currentState.reps + 1, - lapses: currentState.lapses + 1, - }; - } - - /** - * Handle a successful recall (Hard, Good, or Easy) - */ - private handleRecall( - currentState: FSRSState, - grade: ReviewGrade, - retrievabilityR: number - ): FSRSState { - const newS = nextRecallStability( - currentState.stability, - currentState.difficulty, - retrievabilityR, - grade, - this.weights - ); - - const newD = nextDifficulty(currentState.difficulty, grade, this.weights); - - return { - ...currentState, - difficulty: newD, - stability: newS, - state: 'Review', - reps: currentState.reps + 1, - }; - } - - /** - * Get the current retrievability for a state - * - * @param state - FSRS state - * @param elapsedDays - Days since last review (optional, calculated from lastReview if not provided) - * @returns Current retrievability (0-1) - */ - getRetrievability(state: FSRSState, elapsedDays?: number): number { - const days = elapsedDays ?? this.daysSinceReview(state.lastReview); - return retrievability(state.stability, days); - } - - /** - * Preview all possible review outcomes without modifying state - * - * @param state - Current FSRS state - * @param elapsedDays - Days since last review - * @returns Object with results for each grade - */ - previewReviews( - state: FSRSState, - elapsedDays: number = 0 - ): Record<'again' | 'hard' | 'good' | 'easy', ReviewResult> { - return { - again: this.review({ ...state }, Grade.Again, elapsedDays), - hard: this.review({ ...state }, Grade.Hard, elapsedDays), - good: this.review({ ...state }, Grade.Good, elapsedDays), - easy: this.review({ ...state }, Grade.Easy, elapsedDays), - }; - } - - /** - * Calculate days since a review date - */ - private daysSinceReview(lastReview: Date): number { - const now = new Date(); - const diffMs = now.getTime() - lastReview.getTime(); - return Math.max(0, diffMs / (1000 * 60 * 60 * 24)); - } - - /** - * Get scheduler configuration - */ - getConfig(): Readonly { - return { ...this.config }; - } - - /** - * Get scheduler weights - */ - getWeights(): readonly number[] { - return [...this.weights]; - } -} - -// ============================================================================ -// UTILITY FUNCTIONS -// ============================================================================ - -/** - * Clamp a value between min and max - */ -function clamp(value: number, min: number, max: number): number { - return Math.max(min, Math.min(max, value)); -} - -/** - * Convert FSRSState to a JSON-serializable format - */ -export function serializeFSRSState(state: FSRSState): string { - return JSON.stringify({ - ...state, - lastReview: state.lastReview.toISOString(), - }); -} - -/** - * Parse a serialized FSRSState from JSON - */ -export function deserializeFSRSState(json: string): FSRSState { - const parsed = JSON.parse(json) as Record; - - return FSRSStateSchema.parse({ - ...parsed, - lastReview: new Date(parsed['lastReview'] as string), - }); -} - -/** - * Calculate optimal review time based on forgetting index - * - * @param state - Current FSRS state - * @param targetRetention - Target retention rate at review time (default 0.9) - * @returns Days until optimal review - */ -export function optimalReviewTime( - state: FSRSState, - targetRetention: number = FSRS_CONSTANTS.DEFAULT_RETENTION -): number { - return nextInterval(state.stability, targetRetention); -} - -/** - * Determine if a review is due - * - * @param state - Current FSRS state - * @param currentRetention - Optional minimum retention threshold (default: use scheduledDays) - * @returns True if review is due - */ -export function isReviewDue(state: FSRSState, currentRetention?: number): boolean { - const daysSinceReview = - (new Date().getTime() - state.lastReview.getTime()) / (1000 * 60 * 60 * 24); - - if (currentRetention !== undefined) { - const r = retrievability(state.stability, daysSinceReview); - return r < currentRetention; - } - - return daysSinceReview >= state.scheduledDays; -} - -// ============================================================================ -// EXPORTS -// ============================================================================ - -export default FSRSScheduler; diff --git a/packages/core/src/core/index.ts b/packages/core/src/core/index.ts deleted file mode 100644 index 63a2b5e..0000000 --- a/packages/core/src/core/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export * from './config.js'; -export * from './types.js'; -export * from './errors.js'; -export * from './database.js'; -export * from './context-watcher.js'; -export * from './rem-cycle.js'; -export * from './consolidation.js'; -export * from './shadow-self.js'; -export * from './security.js'; -export * from './vector-store.js'; -export * from './fsrs.js'; -export * from './embeddings.js'; diff --git a/packages/core/src/core/rem-cycle.ts b/packages/core/src/core/rem-cycle.ts deleted file mode 100644 index 7225590..0000000 --- a/packages/core/src/core/rem-cycle.ts +++ /dev/null @@ -1,721 +0,0 @@ -/** - * REM Cycle - Nocturnal Optimization with Semantic Understanding - * - * "The brain that dreams while you sleep." - * - * This module discovers connections between unconnected knowledge nodes - * by analyzing semantic similarity, shared concepts, keyword overlap, - * emotional resonance, and spreading activation patterns. - * - * KEY FEATURES: - * 1. Semantic Similarity - Uses embeddings for deep understanding - * 2. Emotional Weighting - Emotionally charged memories create stronger connections - * 3. Spreading Activation - Discovers transitive relationships (A->B->C implies A~C) - * 4. Reconsolidation - Accessing memories strengthens their connections - * 5. Exponential Temporal Proximity - Time-based connection strength decay - */ - -import { VestigeDatabase } from './database.js'; -import type { KnowledgeNode } from './types.js'; -import natural from 'natural'; -import { - createEmbeddingService, - type EmbeddingService, - EmbeddingCache, - cosineSimilarity, -} from './embeddings.js'; - -// ============================================================================ -// TYPES -// ============================================================================ - -type ConnectionType = - | 'concept_overlap' - | 'keyword_similarity' - | 'entity_shared' - | 'temporal_proximity' - | 'semantic_similarity' - | 'spreading_activation'; - -interface DiscoveredConnection { - nodeA: KnowledgeNode; - nodeB: KnowledgeNode; - reason: string; - strength: number; // 0-1 - connectionType: ConnectionType; -} - -interface REMCycleResult { - nodesAnalyzed: number; - connectionsDiscovered: number; - connectionsCreated: number; - spreadingActivationEdges: number; - reconsolidatedNodes: number; - duration: number; - semanticEnabled: boolean; - discoveries: Array<{ - nodeA: string; - nodeB: string; - reason: string; - type: ConnectionType; - }>; -} - -interface REMCycleOptions { - maxAnalyze?: number; - minStrength?: number; - dryRun?: boolean; - /** Enable semantic similarity analysis (requires Ollama) */ - enableSemantic?: boolean; - /** Run spreading activation to discover transitive connections */ - enableSpreadingActivation?: boolean; - /** Maximum depth for spreading activation */ - spreadingActivationDepth?: number; - /** Node IDs that were recently accessed (for reconsolidation) */ - recentlyAccessedIds?: string[]; -} - -// ============================================================================ -// CONSTANTS -// ============================================================================ - -/** Temporal half-life in days for exponential proximity decay */ -const TEMPORAL_HALF_LIFE_DAYS = 7; - -/** Semantic similarity thresholds */ -const SEMANTIC_STRONG_THRESHOLD = 0.7; -const SEMANTIC_MODERATE_THRESHOLD = 0.5; - -/** Weight decay for spreading activation (per hop) */ -const SPREADING_ACTIVATION_DECAY = 0.8; - -/** Reconsolidation strength boost (5%) */ -const RECONSOLIDATION_BOOST = 0.05; - -// ============================================================================ -// SIMILARITY ANALYSIS -// ============================================================================ - -const tokenizer = new natural.WordTokenizer(); - -/** - * Extract keywords from content using TF-IDF - */ -function extractKeywords(content: string): string[] { - const tokens = tokenizer.tokenize(content.toLowerCase()) || []; - - // Filter out common stop words and short tokens - const stopWords = new Set([ - 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', - 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been', - 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', - 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought', - 'used', 'it', 'its', 'this', 'that', 'these', 'those', 'i', 'you', 'he', - 'she', 'we', 'they', 'what', 'which', 'who', 'whom', 'whose', 'where', - 'when', 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more', - 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', - 'same', 'so', 'than', 'too', 'very', 'just', 'also', 'now', 'here', - ]); - - return tokens.filter(token => - token.length > 3 && - !stopWords.has(token) && - !/^\d+$/.test(token) // Filter pure numbers - ); -} - -/** - * Calculate Jaccard similarity between two keyword sets - */ -function jaccardSimilarity(setA: Set, setB: Set): number { - const intersection = new Set([...setA].filter(x => setB.has(x))); - const union = new Set([...setA, ...setB]); - - if (union.size === 0) return 0; - return intersection.size / union.size; -} - -/** - * Find shared concepts between two nodes - */ -function findSharedConcepts(nodeA: KnowledgeNode, nodeB: KnowledgeNode): string[] { - const conceptsA = new Set([...nodeA.concepts, ...nodeA.tags]); - const conceptsB = new Set([...nodeB.concepts, ...nodeB.tags]); - - return [...conceptsA].filter(c => conceptsB.has(c)); -} - -/** - * Find shared people between two nodes - */ -function findSharedPeople(nodeA: KnowledgeNode, nodeB: KnowledgeNode): string[] { - const peopleA = new Set(nodeA.people); - const peopleB = new Set(nodeB.people); - - return [...peopleA].filter(p => peopleB.has(p)); -} - -/** - * Calculate exponential temporal proximity weight - * Uses half-life decay instead of binary same-day check - */ -function calculateTemporalProximity(nodeA: KnowledgeNode, nodeB: KnowledgeNode): number { - const msPerDay = 24 * 60 * 60 * 1000; - const diffMs = Math.abs(nodeA.createdAt.getTime() - nodeB.createdAt.getTime()); - const daysBetween = diffMs / msPerDay; - - // Exponential decay: weight = e^(-t/half_life) - // At t=0: weight = 1.0 - // At t=half_life: weight = 0.5 - // At t=2*half_life: weight = 0.25 - return Math.exp(-daysBetween / TEMPORAL_HALF_LIFE_DAYS); -} - -/** - * Calculate emotional resonance between two nodes - * Returns a boost multiplier (1.0 to 1.5) based on combined emotional intensity - */ -function calculateEmotionalBoost(nodeA: KnowledgeNode, nodeB: KnowledgeNode): number { - const emotionalA = nodeA.sentimentIntensity || 0; - const emotionalB = nodeB.sentimentIntensity || 0; - - // Average emotional intensity - const emotionalResonance = (emotionalA + emotionalB) / 2; - - // Up to 1.5x boost for highly emotional content - return 1 + (emotionalResonance * 0.5); -} - -// ============================================================================ -// SEMANTIC ANALYSIS -// ============================================================================ - -/** - * Analyze semantic connection between two nodes using embeddings - */ -async function analyzeSemanticConnection( - nodeA: KnowledgeNode, - nodeB: KnowledgeNode, - embeddingService: EmbeddingService, - cache: EmbeddingCache -): Promise { - try { - // Get or generate embeddings - let embeddingA = cache.get(nodeA.id); - let embeddingB = cache.get(nodeB.id); - - // Generate missing embeddings - if (!embeddingA) { - embeddingA = await embeddingService.generateEmbedding(nodeA.content); - cache.set(nodeA.id, embeddingA); - } - - if (!embeddingB) { - embeddingB = await embeddingService.generateEmbedding(nodeB.content); - cache.set(nodeB.id, embeddingB); - } - - // Calculate cosine similarity - const similarity = cosineSimilarity(embeddingA, embeddingB); - - // Apply emotional boost - const emotionalBoost = calculateEmotionalBoost(nodeA, nodeB); - const boostedSimilarity = Math.min(1, similarity * emotionalBoost); - - // Strong semantic connection - if (similarity >= SEMANTIC_STRONG_THRESHOLD) { - return { - nodeA, - nodeB, - reason: `Strong semantic similarity (${(similarity * 100).toFixed(0)}%)`, - strength: Math.min(1, boostedSimilarity + 0.2), // Boost for strong connections - connectionType: 'semantic_similarity', - }; - } - - // Moderate semantic connection - if (similarity >= SEMANTIC_MODERATE_THRESHOLD) { - return { - nodeA, - nodeB, - reason: `Moderate semantic similarity (${(similarity * 100).toFixed(0)}%)`, - strength: boostedSimilarity, - connectionType: 'semantic_similarity', - }; - } - - return null; - } catch { - // If embedding fails, return null to fall back to traditional analysis - return null; - } -} - -// ============================================================================ -// TRADITIONAL ANALYSIS (FALLBACK) -// ============================================================================ - -/** - * Analyze potential connection between two nodes using traditional methods - * Used as fallback when embeddings are unavailable - */ -function analyzeTraditionalConnection( - nodeA: KnowledgeNode, - nodeB: KnowledgeNode -): DiscoveredConnection | null { - // Extract keywords from both nodes - const keywordsA = new Set(extractKeywords(nodeA.content)); - const keywordsB = new Set(extractKeywords(nodeB.content)); - - // Calculate keyword similarity - const keywordSim = jaccardSimilarity(keywordsA, keywordsB); - - // Find shared concepts/tags - const sharedConcepts = findSharedConcepts(nodeA, nodeB); - - // Find shared people - const sharedPeople = findSharedPeople(nodeA, nodeB); - - // Calculate temporal proximity weight - const temporalWeight = calculateTemporalProximity(nodeA, nodeB); - - // Calculate emotional boost - const emotionalBoost = calculateEmotionalBoost(nodeA, nodeB); - - // Determine if there's a meaningful connection - // Priority: shared entities > concept overlap > keyword similarity > temporal - - if (sharedPeople.length > 0) { - const baseStrength = Math.min(1, 0.5 + sharedPeople.length * 0.2); - return { - nodeA, - nodeB, - reason: `Shared people: ${sharedPeople.join(', ')}`, - strength: Math.min(1, baseStrength * emotionalBoost), - connectionType: 'entity_shared', - }; - } - - if (sharedConcepts.length >= 2) { - const baseStrength = Math.min(1, 0.4 + sharedConcepts.length * 0.15); - return { - nodeA, - nodeB, - reason: `Shared concepts: ${sharedConcepts.slice(0, 3).join(', ')}`, - strength: Math.min(1, baseStrength * emotionalBoost), - connectionType: 'concept_overlap', - }; - } - - if (keywordSim > 0.15) { - // Find the actual overlapping keywords - const overlap = [...keywordsA].filter(k => keywordsB.has(k)).slice(0, 5); - const baseStrength = Math.min(1, keywordSim * 2); - return { - nodeA, - nodeB, - reason: `Keyword overlap (${(keywordSim * 100).toFixed(0)}%): ${overlap.join(', ')}`, - strength: Math.min(1, baseStrength * emotionalBoost), - connectionType: 'keyword_similarity', - }; - } - - // Temporal proximity with related content - if (temporalWeight > 0.5 && (sharedConcepts.length > 0 || keywordSim > 0.05)) { - const baseStrength = 0.3 + (temporalWeight - 0.5) * 0.4; // Scale 0.3-0.5 - return { - nodeA, - nodeB, - reason: `Created ${Math.round((1 - temporalWeight) * TEMPORAL_HALF_LIFE_DAYS * 2)} days apart with related content`, - strength: Math.min(1, baseStrength * emotionalBoost), - connectionType: 'temporal_proximity', - }; - } - - return null; -} - -// ============================================================================ -// SPREADING ACTIVATION -// ============================================================================ - -interface SpreadingActivationResult { - edgesCreated: number; - paths: Array<{ - from: string; - via: string; - to: string; - weight: number; - }>; -} - -/** - * Apply spreading activation to discover transitive connections - * If A -> B and B -> C exist, creates A -> C with decayed weight - */ -function applySpreadingActivation( - db: VestigeDatabase, - maxDepth: number = 2, - minWeight: number = 0.2 -): SpreadingActivationResult { - const result: SpreadingActivationResult = { - edgesCreated: 0, - paths: [], - }; - - // Get all existing edges - const edges = db['db'].prepare(` - SELECT from_id, to_id, weight FROM graph_edges - WHERE edge_type = 'similar_to' - `).all() as { from_id: string; to_id: string; weight: number }[]; - - // Build adjacency map (bidirectional) - const adjacency = new Map>(); - - for (const edge of edges) { - // Forward direction - if (!adjacency.has(edge.from_id)) { - adjacency.set(edge.from_id, new Map()); - } - adjacency.get(edge.from_id)!.set(edge.to_id, edge.weight); - - // Reverse direction (treat as undirected) - if (!adjacency.has(edge.to_id)) { - adjacency.set(edge.to_id, new Map()); - } - adjacency.get(edge.to_id)!.set(edge.from_id, edge.weight); - } - - // Find existing direct connections (to avoid duplicates) - const existingConnections = new Set(); - for (const edge of edges) { - existingConnections.add(`${edge.from_id}-${edge.to_id}`); - existingConnections.add(`${edge.to_id}-${edge.from_id}`); - } - - // For each node, find 2-hop paths - const newConnections: Array<{ - from: string; - to: string; - via: string; - weight: number; - }> = []; - - for (const [nodeA, neighborsA] of adjacency) { - for (const [nodeB, weightAB] of neighborsA) { - const neighborsB = adjacency.get(nodeB); - if (!neighborsB) continue; - - for (const [nodeC, weightBC] of neighborsB) { - // Skip if A == C or if direct connection already exists - if (nodeA === nodeC) continue; - - const connectionKey = `${nodeA}-${nodeC}`; - const reverseKey = `${nodeC}-${nodeA}`; - - if (existingConnections.has(connectionKey) || existingConnections.has(reverseKey)) { - continue; - } - - // Calculate transitive weight with decay - const transitiveWeight = weightAB * weightBC * SPREADING_ACTIVATION_DECAY; - - if (transitiveWeight >= minWeight) { - newConnections.push({ - from: nodeA, - to: nodeC, - via: nodeB, - weight: transitiveWeight, - }); - - // Mark as existing to avoid duplicates - existingConnections.add(connectionKey); - existingConnections.add(reverseKey); - } - } - } - } - - // Create the new edges - for (const conn of newConnections) { - try { - db.insertEdge({ - fromId: conn.from, - toId: conn.to, - edgeType: 'similar_to', - weight: conn.weight, - metadata: { - discoveredBy: 'spreading_activation', - viaNode: conn.via, - connectionType: 'spreading_activation', - }, - createdAt: new Date(), - }); - - result.edgesCreated++; - result.paths.push(conn); - } catch { - // Edge might already exist, skip - } - } - - return result; -} - -// ============================================================================ -// RECONSOLIDATION -// ============================================================================ - -/** - * Strengthen connections for recently accessed nodes - * Implements memory reconsolidation - accessing memories makes them stronger - */ -function reconsolidateConnections(db: VestigeDatabase, nodeId: string): number { - let strengthened = 0; - - try { - // Get all edges involving this node - const edges = db['db'].prepare(` - SELECT id, weight FROM graph_edges - WHERE from_id = ? OR to_id = ? - `).all(nodeId, nodeId) as { id: string; weight: number }[]; - - // Strengthen each edge by RECONSOLIDATION_BOOST (5%) - const updateStmt = db['db'].prepare(` - UPDATE graph_edges - SET weight = MIN(1.0, weight * ?) - WHERE id = ? - `); - - for (const edge of edges) { - const newWeight = Math.min(1.0, edge.weight * (1 + RECONSOLIDATION_BOOST)); - if (newWeight > edge.weight) { - updateStmt.run(newWeight, edge.id); - strengthened++; - } - } - } catch { - // Reconsolidation is optional, don't fail the cycle - } - - return strengthened; -} - -// ============================================================================ -// REM CYCLE MAIN LOGIC -// ============================================================================ - -/** - * Get nodes that have few or no connections - */ -function getDisconnectedNodes(db: VestigeDatabase, maxEdges: number = 1): KnowledgeNode[] { - // Get all nodes - const result = db.getRecentNodes({ limit: 500 }); - const allNodes = result.items; - - // Filter to nodes with few connections - const disconnected: KnowledgeNode[] = []; - - for (const node of allNodes) { - const related = db.getRelatedNodes(node.id, 1); - if (related.length <= maxEdges) { - disconnected.push(node); - } - } - - return disconnected; -} - -/** - * Run one REM cycle - discover and create connections - * - * The cycle performs these steps: - * 1. Reconsolidate recently accessed nodes (strengthen existing connections) - * 2. Find disconnected nodes - * 3. Try semantic similarity first (if enabled and available) - * 4. Fall back to traditional analysis (Jaccard, shared concepts, etc.) - * 5. Apply emotional weighting to all connections - * 6. Run spreading activation to find transitive connections - */ -export async function runREMCycle( - db: VestigeDatabase, - options: REMCycleOptions = {} -): Promise { - const startTime = Date.now(); - const { - maxAnalyze = 50, - minStrength = 0.3, - dryRun = false, - enableSemantic = true, - enableSpreadingActivation = true, - spreadingActivationDepth = 2, - recentlyAccessedIds = [], - } = options; - - const result: REMCycleResult = { - nodesAnalyzed: 0, - connectionsDiscovered: 0, - connectionsCreated: 0, - spreadingActivationEdges: 0, - reconsolidatedNodes: 0, - duration: 0, - semanticEnabled: false, - discoveries: [], - }; - - // Step 1: Reconsolidate recently accessed nodes - if (!dryRun && recentlyAccessedIds.length > 0) { - for (const nodeId of recentlyAccessedIds) { - const strengthened = reconsolidateConnections(db, nodeId); - if (strengthened > 0) { - result.reconsolidatedNodes++; - } - } - } - - // Step 2: Initialize embedding service if semantic analysis is enabled - let embeddingService: EmbeddingService | null = null; - let embeddingCache: EmbeddingCache | null = null; - - if (enableSemantic) { - try { - embeddingService = await createEmbeddingService(); - const isAvailable = await embeddingService.isAvailable(); - result.semanticEnabled = isAvailable; - - if (isAvailable) { - embeddingCache = new EmbeddingCache(500, 30); // 500 entries, 30 min TTL - } - } catch { - // Semantic analysis not available, continue without it - result.semanticEnabled = false; - } - } - - // Step 3: Get disconnected nodes - const disconnected = getDisconnectedNodes(db, 2); - - if (disconnected.length < 2) { - result.duration = Date.now() - startTime; - return result; - } - - // Limit analysis - const toAnalyze = disconnected.slice(0, maxAnalyze); - result.nodesAnalyzed = toAnalyze.length; - - // Step 4: Compare pairs - const discoveries: DiscoveredConnection[] = []; - const analyzed = new Set(); - - for (let i = 0; i < toAnalyze.length; i++) { - for (let j = i + 1; j < toAnalyze.length; j++) { - const nodeA = toAnalyze[i]; - const nodeB = toAnalyze[j]; - - if (!nodeA || !nodeB) continue; - - // Skip if already have an edge - const pairKey = [nodeA.id, nodeB.id].sort().join('-'); - if (analyzed.has(pairKey)) continue; - analyzed.add(pairKey); - - let connection: DiscoveredConnection | null = null; - - // Try semantic similarity first if available - if (result.semanticEnabled && embeddingService && embeddingCache) { - connection = await analyzeSemanticConnection( - nodeA, - nodeB, - embeddingService, - embeddingCache - ); - } - - // Fall back to traditional analysis if no semantic connection found - if (!connection) { - connection = analyzeTraditionalConnection(nodeA, nodeB); - } - - if (connection && connection.strength >= minStrength) { - discoveries.push(connection); - } - } - } - - result.connectionsDiscovered = discoveries.length; - - // Step 5: Create edges for discovered connections - if (!dryRun) { - for (const discovery of discoveries) { - try { - db.insertEdge({ - fromId: discovery.nodeA.id, - toId: discovery.nodeB.id, - edgeType: 'similar_to', - weight: discovery.strength, - metadata: { - discoveredBy: 'rem_cycle', - reason: discovery.reason, - connectionType: discovery.connectionType, - }, - createdAt: new Date(), - }); - result.connectionsCreated++; - - result.discoveries.push({ - nodeA: discovery.nodeA.content.slice(0, 50), - nodeB: discovery.nodeB.content.slice(0, 50), - reason: discovery.reason, - type: discovery.connectionType, - }); - } catch { - // Edge might already exist - } - } - - // Step 6: Apply spreading activation - if (enableSpreadingActivation) { - const spreadingResult = applySpreadingActivation(db, spreadingActivationDepth, minStrength); - result.spreadingActivationEdges = spreadingResult.edgesCreated; - - // Add spreading activation discoveries to results - for (const path of spreadingResult.paths) { - result.discoveries.push({ - nodeA: path.from.slice(0, 20), - nodeB: path.to.slice(0, 20), - reason: `Transitive via ${path.via.slice(0, 20)} (${(path.weight * 100).toFixed(0)}%)`, - type: 'spreading_activation', - }); - } - } - } else { - // Dry run - just record discoveries - for (const discovery of discoveries) { - result.discoveries.push({ - nodeA: discovery.nodeA.content.slice(0, 50), - nodeB: discovery.nodeB.content.slice(0, 50), - reason: discovery.reason, - type: discovery.connectionType, - }); - } - } - - result.duration = Date.now() - startTime; - return result; -} - -/** - * Get a summary of potential discoveries without creating edges - */ -export async function previewREMCycle(db: VestigeDatabase): Promise { - return runREMCycle(db, { dryRun: true, maxAnalyze: 100 }); -} - -/** - * Trigger reconsolidation for a specific node - * Call this when a node is accessed to strengthen its connections - */ -export function triggerReconsolidation(db: VestigeDatabase, nodeId: string): number { - return reconsolidateConnections(db, nodeId); -} diff --git a/packages/core/src/core/security.ts b/packages/core/src/core/security.ts deleted file mode 100644 index 4d44dd5..0000000 --- a/packages/core/src/core/security.ts +++ /dev/null @@ -1,1013 +0,0 @@ -/** - * Security Utilities for Vestige - * - * Provides comprehensive security controls including: - * - Input validation and sanitization - * - Path traversal prevention - * - SSRF prevention (including IPv6 and DNS rebinding) - * - Unicode homograph detection - * - Symlink race condition prevention - * - Rate limiting - * - SQL injection prevention - * - Security event logging - */ - -import path from 'path'; -import os from 'os'; -import fs from 'fs'; -import { URL } from 'url'; -import { SecurityError } from './errors.js'; - -// ============================================================================ -// CONSTANTS -// ============================================================================ - -/** - * Allowed base directories for file operations - * Users can only read/write files within these directories - */ -const ALLOWED_BASE_DIRS = [ - os.homedir(), // User's home directory - '/tmp', // Temp directory - process.cwd(), // Current working directory -]; - -/** - * Sensitive paths that should NEVER be accessible - */ -const BLOCKED_PATHS = [ - '/.ssh', - '/.gnupg', - '/.aws', - '/.config/gcloud', - '/.azure', - '/etc/passwd', - '/etc/shadow', - '/etc/hosts', - '/.env', - '/.git/config', - '/id_rsa', - '/id_ed25519', - '/.netrc', - '/.npmrc', - '/.pypirc', - '/.docker/config.json', - '/.kube/config', - '/credentials', - '/secrets', -]; - -/** - * Blocked file extensions - */ -const BLOCKED_EXTENSIONS = [ - '.pem', - '.key', - '.p12', - '.pfx', - '.keystore', - '.jks', - '.crt', - '.cer', -]; - -/** - * Private/internal IPv4 ranges that should be blocked (SSRF prevention) - */ -const PRIVATE_IPV4_PATTERNS = [ - /^127\./, // Loopback - /^10\./, // Private Class A - /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private Class B - /^192\.168\./, // Private Class C - /^169\.254\./, // Link-local - /^0\./, // Current network - /^100\.(6[4-9]|[7-9][0-9]|1[0-2][0-9])\./, // Carrier-grade NAT (100.64.0.0/10) - /^198\.1[89]\./, // Benchmark testing - /^192\.0\.0\./, // IANA special purpose - /^192\.0\.2\./, // TEST-NET-1 - /^198\.51\.100\./, // TEST-NET-2 - /^203\.0\.113\./, // TEST-NET-3 - /^224\./, // Multicast - /^240\./, // Reserved -]; - -/** - * Private/internal IPv6 ranges that should be blocked - * Fixed: Comprehensive IPv6 private range detection - */ -const PRIVATE_IPV6_PATTERNS = [ - /^::1$/i, // Loopback (exact match) - /^::$/, // Unspecified address - /^::ffff:/i, // IPv4-mapped IPv6 - /^fe80:/i, // Link-local - /^fec0:/i, // Site-local (deprecated but still dangerous) - /^fc00:/i, // Unique local (ULA) - /^fd[0-9a-f]{2}:/i, // Unique local (ULA) - fd00::/8 - /^ff[0-9a-f]{2}:/i, // Multicast - /^2001:db8:/i, // Documentation prefix - /^2001:10:/i, // ORCHID - /^2001:20:/i, // ORCHIDv2 - /^100::/i, // Discard prefix - /^64:ff9b:/i, // NAT64 - /^\[::1\]$/i, // Bracketed loopback - /^\[::ffff:/i, // Bracketed IPv4-mapped - /^\[fe80:/i, // Bracketed link-local - /^\[fc00:/i, // Bracketed ULA - /^\[fd[0-9a-f]{2}:/i, // Bracketed ULA -]; - -/** - * Blocked hostnames for SSRF prevention - */ -const BLOCKED_HOSTNAMES = [ - 'localhost', - 'localhost.localdomain', - '0.0.0.0', - '[::1]', - '[::0]', - '[::]', - 'metadata.google.internal', // GCP metadata - 'metadata.google.com', - '169.254.169.254', // AWS/GCP/Azure metadata - 'instance-data', // AWS metadata alias - 'metadata', // Generic metadata - 'metadata.internal', - 'computeMetadata', - '169.254.170.2', // AWS ECS task metadata - 'fd00:ec2::254', // AWS IPv6 metadata -]; - -/** - * Numeric localhost variations (hex, octal, decimal) - */ -const LOCALHOST_NUMERIC_PATTERNS = [ - /^0x7f/i, // Hex 127.x.x.x - /^2130706433$/, // Decimal 127.0.0.1 - /^017700000001$/, // Octal 127.0.0.1 - /^0177\.0*\.0*\.0*1$/, // Octal dotted - /^0x7f\.0x0+\.0x0+\.0x0*1$/i, // Hex dotted -]; - -/** - * Allowed URL protocols - */ -const ALLOWED_PROTOCOLS = ['http:', 'https:']; - -// ============================================================================ -// PATH SECURITY -// ============================================================================ - -export interface PathValidationResult { - valid: boolean; - sanitizedPath: string | null; - error: string | null; -} - -/** - * Validate and sanitize a file path - * Prevents path traversal attacks and access to sensitive files - * - * Security improvements: - * - Symlink resolution to prevent TOCTOU race conditions - * - Null byte detection - * - More comprehensive sensitive path detection - */ -export function validatePath(inputPath: string): PathValidationResult { - try { - // Check for null bytes (CWE-158) - if (inputPath.includes('\0')) { - logSecurityEvent({ - type: 'path_traversal', - details: { reason: 'null_byte_detected' }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedPath: null, - error: 'Invalid path: null byte detected', - }; - } - - // Resolve to absolute path - const absolutePath = path.resolve(inputPath); - const normalizedPath = path.normalize(absolutePath); - - // Check for path traversal attempts - if (inputPath.includes('..') && !normalizedPath.startsWith(process.cwd())) { - // Allow .. only if it resolves within cwd - const relative = path.relative(process.cwd(), normalizedPath); - if (relative.startsWith('..')) { - logSecurityEvent({ - type: 'path_traversal', - details: { inputPath, resolvedPath: normalizedPath }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedPath: null, - error: 'Path traversal detected: cannot access files outside allowed directories', - }; - } - } - - // Check if path is within allowed directories - const isAllowed = ALLOWED_BASE_DIRS.some(baseDir => - normalizedPath.startsWith(path.resolve(baseDir)) - ); - - if (!isAllowed) { - logSecurityEvent({ - type: 'path_traversal', - details: { reason: 'outside_allowed_dirs', normalizedPath }, - severity: 'medium', - blocked: true, - }); - return { - valid: false, - sanitizedPath: null, - error: 'Access denied: path must be within home directory, /tmp, or current working directory', - }; - } - - // Check for sensitive paths - const lowerPath = normalizedPath.toLowerCase(); - for (const blocked of BLOCKED_PATHS) { - if (lowerPath.includes(blocked.toLowerCase())) { - logSecurityEvent({ - type: 'path_traversal', - details: { reason: 'sensitive_path', blocked }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedPath: null, - error: 'Access denied: cannot access sensitive system files', - }; - } - } - - // Check for blocked extensions - const ext = path.extname(normalizedPath).toLowerCase(); - if (BLOCKED_EXTENSIONS.includes(ext)) { - logSecurityEvent({ - type: 'path_traversal', - details: { reason: 'blocked_extension', extension: ext }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedPath: null, - error: 'Access denied: cannot access credential files', - }; - } - - return { - valid: true, - sanitizedPath: normalizedPath, - error: null, - }; - } catch (error) { - return { - valid: false, - sanitizedPath: null, - error: 'Invalid path', - }; - } -} - -/** - * Resolve symlinks and verify the path is still safe - * Prevents TOCTOU (Time-of-Check Time-of-Use) race conditions - */ -export function validatePathWithSymlinkResolution(inputPath: string): PathValidationResult { - // First do basic validation - const basicResult = validatePath(inputPath); - if (!basicResult.valid) { - return basicResult; - } - - try { - // Check if path exists - if (!fs.existsSync(basicResult.sanitizedPath!)) { - // Path doesn't exist yet, that's okay for new files - return basicResult; - } - - // Resolve symlinks to get the real path - const realPath = fs.realpathSync(basicResult.sanitizedPath!); - - // Now validate the REAL path (after symlink resolution) - const realPathResult = validatePath(realPath); - if (!realPathResult.valid) { - logSecurityEvent({ - type: 'path_traversal', - details: { - reason: 'symlink_escape', - inputPath, - resolvedPath: realPath - }, - severity: 'critical', - blocked: true, - }); - return { - valid: false, - sanitizedPath: null, - error: 'Access denied: symlink points outside allowed directories', - }; - } - - return { - valid: true, - sanitizedPath: realPath, - error: null, - }; - } catch (error) { - // If realpath fails, the path might not exist yet - // In that case, return the basic validation result - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return basicResult; - } - return { - valid: false, - sanitizedPath: null, - error: 'Failed to resolve path', - }; - } -} - -// ============================================================================ -// URL SECURITY (SSRF Prevention) -// ============================================================================ - -export interface UrlValidationResult { - valid: boolean; - sanitizedUrl: string | null; - error: string | null; -} - -/** - * Check if a hostname contains Unicode homograph characters - * Detects IDN homograph attacks (e.g., using Cyrillic 'а' instead of Latin 'a') - */ -export function containsHomographs(hostname: string): boolean { - // Convert to ASCII (punycode) and compare - // If they differ significantly, there might be homograph characters - try { - const url = new URL(`http://${hostname}`); - const asciiHostname = url.hostname; - - // Check for mixed scripts - // These Unicode ranges indicate potential homograph attacks - const suspiciousPatterns = [ - /[\u0400-\u04FF]/, // Cyrillic - /[\u0370-\u03FF]/, // Greek - /[\u0530-\u058F]/, // Armenian - /[\u10A0-\u10FF]/, // Georgian - ]; - - for (const pattern of suspiciousPatterns) { - if (pattern.test(hostname)) { - // Check if the hostname also contains Latin characters - if (/[a-zA-Z]/.test(hostname)) { - return true; // Mixed scripts detected - } - } - } - - // Check for look-alike characters that commonly appear in phishing - const homoglyphs: Record = { - 'a': ['а', 'ɑ', 'α'], // Latin a vs Cyrillic а, etc. - 'e': ['е', 'ε'], - 'o': ['о', 'ο', '0'], - 'p': ['р', 'ρ'], - 'c': ['с', 'ϲ'], - 'x': ['х', 'χ'], - 'y': ['у', 'γ'], - 'n': ['п'], - 's': ['ѕ'], - }; - - for (const [latin, lookalikes] of Object.entries(homoglyphs)) { - for (const lookalike of lookalikes) { - if (hostname.includes(lookalike) && hostname.includes(latin)) { - return true; - } - } - } - - return false; - } catch { - return false; - } -} - -/** - * Validate and sanitize a URL - * Prevents SSRF attacks by blocking internal/private addresses - * - * Security improvements: - * - Comprehensive IPv6 detection - * - Homograph attack detection - * - DNS rebinding protection hints - */ -export function validateUrl(inputUrl: string): UrlValidationResult { - try { - // Parse the URL - const url = new URL(inputUrl); - - // Check protocol - if (!ALLOWED_PROTOCOLS.includes(url.protocol)) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { reason: 'invalid_protocol', protocol: url.protocol }, - severity: 'medium', - blocked: true, - }); - return { - valid: false, - sanitizedUrl: null, - error: 'Invalid protocol: only HTTP and HTTPS are allowed', - }; - } - - const hostname = url.hostname.toLowerCase(); - - // Check for blocked hostnames - if (BLOCKED_HOSTNAMES.includes(hostname)) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { reason: 'blocked_hostname', hostname }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedUrl: null, - error: 'Blocked hostname: internal addresses are not allowed', - }; - } - - // Check for private/internal IPv4 - for (const pattern of PRIVATE_IPV4_PATTERNS) { - if (pattern.test(hostname)) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { reason: 'private_ipv4', hostname }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedUrl: null, - error: 'Blocked: private IPv4 address detected', - }; - } - } - - // Check for private/internal IPv6 - for (const pattern of PRIVATE_IPV6_PATTERNS) { - if (pattern.test(hostname)) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { reason: 'private_ipv6', hostname }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedUrl: null, - error: 'Blocked: private IPv6 address detected', - }; - } - } - - // Check for numeric localhost variations - for (const pattern of LOCALHOST_NUMERIC_PATTERNS) { - if (pattern.test(hostname)) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { reason: 'encoded_localhost', hostname }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedUrl: null, - error: 'Blocked: encoded localhost address detected', - }; - } - } - - // Check for Unicode homograph attacks - if (containsHomographs(hostname)) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { reason: 'homograph_attack', hostname }, - severity: 'high', - blocked: true, - }); - return { - valid: false, - sanitizedUrl: null, - error: 'Blocked: potential homograph attack detected in hostname', - }; - } - - // Check for suspicious URL-encoded characters - if (inputUrl.includes('%00') || inputUrl.includes('%0d') || inputUrl.includes('%0a')) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { reason: 'suspicious_encoding' }, - severity: 'medium', - blocked: true, - }); - return { - valid: false, - sanitizedUrl: null, - error: 'Blocked: suspicious URL encoding detected', - }; - } - - // Check for @ symbol which could be used for credential injection - if (url.username || url.password) { - logSecurityEvent({ - type: 'ssrf_attempt', - details: { reason: 'credentials_in_url' }, - severity: 'medium', - blocked: true, - }); - return { - valid: false, - sanitizedUrl: null, - error: 'Blocked: credentials in URL are not allowed', - }; - } - - // Reconstruct clean URL (removes any weird encoding tricks) - const cleanUrl = url.toString(); - - return { - valid: true, - sanitizedUrl: cleanUrl, - error: null, - }; - } catch (error) { - return { - valid: false, - sanitizedUrl: null, - error: 'Invalid URL format', - }; - } -} - -// ============================================================================ -// INPUT SANITIZATION -// ============================================================================ - -/** - * Maximum content length to prevent DoS - */ -export const MAX_CONTENT_LENGTH = 10 * 1024 * 1024; // 10MB - -export interface SanitizeInputOptions { - maxLength?: number; - allowedChars?: RegExp; - stripHtml?: boolean; - normalizeUnicode?: boolean; - allowNewlines?: boolean; -} - -/** - * Comprehensive input sanitization - */ -export function sanitizeInput(input: string, options: SanitizeInputOptions = {}): string { - const { - maxLength = MAX_CONTENT_LENGTH, - allowedChars, - stripHtml = false, - normalizeUnicode = true, - allowNewlines = true, - } = options; - - let sanitized = input; - - // Truncate to max length - if (sanitized.length > maxLength) { - sanitized = sanitized.slice(0, maxLength); - logSecurityEvent({ - type: 'validation_failure', - details: { reason: 'content_truncated', originalLength: input.length, maxLength }, - severity: 'low', - blocked: false, - }); - } - - // Remove null bytes - sanitized = sanitized.replace(/\x00/g, ''); - - // Remove other control characters (except newlines and tabs if allowed) - if (allowNewlines) { - sanitized = sanitized.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); - } else { - sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, ''); - } - - // Strip HTML tags if requested - if (stripHtml) { - sanitized = sanitized.replace(/<[^>]*>/g, ''); - } - - // Normalize Unicode if requested (NFC normalization) - if (normalizeUnicode) { - sanitized = sanitized.normalize('NFC'); - } - - // Apply allowed character filter if specified - if (allowedChars) { - sanitized = sanitized - .split('') - .filter(char => allowedChars.test(char)) - .join(''); - } - - return sanitized; -} - -/** - * Sanitize text content by removing potentially dangerous characters - */ -export function sanitizeContent(content: string, maxLength: number = MAX_CONTENT_LENGTH): string { - return sanitizeInput(content, { maxLength, allowNewlines: true }); -} - -/** - * Validate that a string is safe for use as an identifier - */ -export function isValidIdentifier(input: string, maxLength: number = 100): boolean { - if (!input || input.length > maxLength) return false; - // Only allow alphanumeric, underscore, hyphen - return /^[a-zA-Z0-9_-]+$/.test(input); -} - -// ============================================================================ -// INPUT VALIDATORS -// ============================================================================ - -/** - * Comprehensive validators for common input types - */ -export const validators = { - /** Validate nanoid format (21 alphanumeric characters) */ - nodeId: (id: string): boolean => /^[a-zA-Z0-9_-]{21}$/.test(id), - - /** Validate tag (max 100 chars, no HTML special chars) */ - tag: (tag: string): boolean => tag.length > 0 && tag.length <= 100 && !/[<>]/.test(tag), - - /** Validate email address */ - email: (email: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && email.length <= 254, - - /** Validate URL */ - url: (url: string): boolean => validateUrl(url).valid, - - /** Validate file path */ - path: (pathStr: string): boolean => validatePath(pathStr).valid, - - /** Validate name (no special chars, reasonable length) */ - name: (name: string): boolean => name.length > 0 && name.length <= 500 && !/[<>"'`;]/.test(name), - - /** Validate positive integer */ - positiveInt: (value: number): boolean => Number.isInteger(value) && value > 0, - - /** Validate percentage (0-100) */ - percentage: (value: number): boolean => typeof value === 'number' && value >= 0 && value <= 100, - - /** Validate UUID */ - uuid: (id: string): boolean => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id), -}; - -// ============================================================================ -// RATE LIMITING -// ============================================================================ - -export interface RateLimitResult { - allowed: boolean; - remaining: number; - resetAt: Date; - retryAfterMs?: number; -} - -/** - * Sliding window rate limiter - */ -export class RateLimiter { - private requests: Map = new Map(); - private cleanupInterval: NodeJS.Timeout | null = null; - - constructor( - private readonly maxRequests: number, - private readonly windowMs: number - ) { - // Cleanup old entries periodically - this.cleanupInterval = setInterval(() => this.cleanup(), windowMs); - } - - /** - * Check if a request is allowed - */ - isAllowed(key: string): RateLimitResult { - const now = Date.now(); - const windowStart = now - this.windowMs; - - // Get existing requests for this key - let timestamps = this.requests.get(key) || []; - - // Filter to only requests within the window - timestamps = timestamps.filter(ts => ts > windowStart); - - const allowed = timestamps.length < this.maxRequests; - const remaining = Math.max(0, this.maxRequests - timestamps.length - (allowed ? 1 : 0)); - const resetAt = new Date(now + this.windowMs); - - if (allowed) { - timestamps.push(now); - this.requests.set(key, timestamps); - } else { - // Calculate when the oldest request will expire - const oldestRequest = Math.min(...timestamps); - const retryAfterMs = oldestRequest + this.windowMs - now; - - logSecurityEvent({ - type: 'rate_limit', - details: { key, requestCount: timestamps.length, maxRequests: this.maxRequests }, - severity: 'medium', - blocked: true, - }); - - return { - allowed: false, - remaining: 0, - resetAt, - retryAfterMs, - }; - } - - return { - allowed: true, - remaining, - resetAt, - }; - } - - /** - * Get current request count for a key - */ - getRequestCount(key: string): number { - const windowStart = Date.now() - this.windowMs; - const timestamps = this.requests.get(key) || []; - return timestamps.filter(ts => ts > windowStart).length; - } - - /** - * Reset rate limit for a specific key - */ - reset(key: string): void { - this.requests.delete(key); - } - - /** - * Clear all rate limit data - */ - clear(): void { - this.requests.clear(); - } - - /** - * Cleanup old entries - */ - private cleanup(): void { - const windowStart = Date.now() - this.windowMs; - for (const [key, timestamps] of this.requests.entries()) { - const valid = timestamps.filter(ts => ts > windowStart); - if (valid.length === 0) { - this.requests.delete(key); - } else { - this.requests.set(key, valid); - } - } - } - - /** - * Stop the cleanup interval - */ - destroy(): void { - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = null; - } - this.requests.clear(); - } -} - -// ============================================================================ -// SQL INJECTION PREVENTION -// ============================================================================ - -export interface PreparedQuery { - sql: string; - values: unknown[]; -} - -/** - * Safe query builder that enforces parameterized queries - * Uses named parameters for clarity and safety - */ -export function prepareQuery( - template: string, - params: Record -): PreparedQuery { - const values: unknown[] = []; - let paramIndex = 0; - - // Replace :paramName with ? and collect values - const sql = template.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => { - if (!(paramName in params)) { - throw new SecurityError(`Missing query parameter: ${paramName}`); - } - values.push(params[paramName]); - paramIndex++; - return '?'; - }); - - return { sql, values }; -} - -/** - * Escape a string for use in LIKE queries - */ -export function escapeLikePattern(pattern: string): string { - return pattern - .replace(/\\/g, '\\\\') // Escape backslashes first - .replace(/%/g, '\\%') // Escape percent - .replace(/_/g, '\\_'); // Escape underscore -} - -/** - * Validate that a query is safe (no dangerous operations) - * This is a defense-in-depth measure - */ -export function isQuerySafe(query: string): boolean { - const dangerousPatterns = [ - /;\s*(DROP|DELETE|TRUNCATE|ALTER|CREATE|INSERT|UPDATE)\s/i, - /--\s*$/m, // SQL comments at end of line - /\/\*[\s\S]*?\*\//, // Block comments - /\bEXEC\s*\(/i, // EXEC function - /\bxp_/i, // SQL Server extended procedures - /\bsp_/i, // SQL Server system procedures - /\bUNION\s+ALL\s+SELECT/i, // UNION injection - ]; - - for (const pattern of dangerousPatterns) { - if (pattern.test(query)) { - logSecurityEvent({ - type: 'validation_failure', - details: { reason: 'dangerous_query_pattern' }, - severity: 'critical', - blocked: true, - }); - return false; - } - } - - return true; -} - -// ============================================================================ -// SECURITY EVENT LOGGING -// ============================================================================ - -export interface SecurityEvent { - type: 'path_traversal' | 'ssrf_attempt' | 'rate_limit' | 'validation_failure'; - timestamp: Date; - details: Record; - severity: 'low' | 'medium' | 'high' | 'critical'; - blocked: boolean; -} - -const securityLog: SecurityEvent[] = []; -const MAX_LOG_SIZE = 1000; - -/** - * Log a security event - */ -export function logSecurityEvent(event: Omit): void { - const fullEvent: SecurityEvent = { - ...event, - timestamp: new Date(), - }; - - securityLog.push(fullEvent); - - // Keep log from growing too large - if (securityLog.length > MAX_LOG_SIZE) { - securityLog.shift(); - } - - // Log to stderr in debug mode - if (process.env['VESTIGE_DEBUG']) { - console.error(`[SECURITY:${fullEvent.severity.toUpperCase()}] ${fullEvent.type}: ${JSON.stringify(fullEvent.details)}`); - } - - // Alert on critical events - if (fullEvent.severity === 'critical') { - console.error(`[SECURITY:CRITICAL] ${fullEvent.type}: ${JSON.stringify(fullEvent.details)}`); - } -} - -/** - * Get recent security events - */ -export function getSecurityEvents(limit: number = 100): SecurityEvent[] { - return securityLog.slice(-limit); -} - -/** - * Get security events by type - */ -export function getSecurityEventsByType(type: SecurityEvent['type'], limit: number = 100): SecurityEvent[] { - return securityLog - .filter(event => event.type === type) - .slice(-limit); -} - -/** - * Get security events by severity - */ -export function getSecurityEventsBySeverity( - severity: SecurityEvent['severity'], - limit: number = 100 -): SecurityEvent[] { - return securityLog - .filter(event => event.severity === severity) - .slice(-limit); -} - -/** - * Clear security log (useful for testing) - */ -export function clearSecurityLog(): void { - securityLog.length = 0; -} - -// ============================================================================ -// SECURITY HEADERS (For Future Web UI) -// ============================================================================ - -export const SECURITY_HEADERS = { - 'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'", - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - 'Permissions-Policy': 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()', - 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', - 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0', -}; - -/** - * Apply security headers to a response object - */ -export function applySecurityHeaders(headers: Record): Record { - return { - ...headers, - ...SECURITY_HEADERS, - }; -} - -// ============================================================================ -// CRYPTO UTILITIES -// ============================================================================ - -/** - * Generate a cryptographically secure random string - */ -export function generateSecureToken(length: number = 32): string { - const crypto = require('crypto'); - return crypto.randomBytes(length).toString('hex'); -} - -/** - * Constant-time string comparison to prevent timing attacks - */ -export function secureCompare(a: string, b: string): boolean { - const crypto = require('crypto'); - if (a.length !== b.length) { - return false; - } - return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); -} diff --git a/packages/core/src/core/shadow-self.ts b/packages/core/src/core/shadow-self.ts deleted file mode 100644 index 2728132..0000000 --- a/packages/core/src/core/shadow-self.ts +++ /dev/null @@ -1,403 +0,0 @@ -/** - * The Shadow Self - Unsolved Problems Queue - * - * "Your subconscious that keeps working while you're not looking." - * - * When you say "I don't know how to fix this," Vestige logs it. - * The Shadow periodically re-attacks these problems with new context. - * - * This turns Vestige from a passive memory into an active problem-solver. - */ - -import Database from 'better-sqlite3'; -import { nanoid } from 'nanoid'; -import path from 'path'; -import fs from 'fs'; -import os from 'os'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface UnsolvedProblem { - id: string; - description: string; - context: string; // Original context when problem was logged - tags: string[]; - status: 'open' | 'investigating' | 'solved' | 'abandoned'; - priority: number; // 1-5, higher = more urgent - attempts: number; // How many times Shadow has tried to solve it - lastAttemptAt: Date | null; - createdAt: Date; - updatedAt: Date; - solution: string | null; // If solved, what was the solution? - relatedNodeIds: string[]; // Knowledge nodes that might help -} - -export interface ShadowInsight { - problemId: string; - insight: string; - source: 'keyword_match' | 'new_knowledge' | 'pattern_recognition'; - confidence: number; - relatedNodeIds: string[]; - createdAt: Date; -} - -// ============================================================================ -// DATABASE SETUP -// ============================================================================ - -const SHADOW_DB_PATH = path.join(os.homedir(), '.vestige', 'shadow.db'); - -function initializeShadowDb(): Database.Database { - const dir = path.dirname(SHADOW_DB_PATH); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - const db = new Database(SHADOW_DB_PATH); - - db.pragma('journal_mode = WAL'); - db.pragma('busy_timeout = 5000'); - - // Unsolved problems table - db.exec(` - CREATE TABLE IF NOT EXISTS unsolved_problems ( - id TEXT PRIMARY KEY, - description TEXT NOT NULL, - context TEXT, - tags TEXT DEFAULT '[]', - status TEXT DEFAULT 'open', - priority INTEGER DEFAULT 3, - attempts INTEGER DEFAULT 0, - last_attempt_at TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - solution TEXT, - related_node_ids TEXT DEFAULT '[]' - ); - - CREATE INDEX IF NOT EXISTS idx_problems_status ON unsolved_problems(status); - CREATE INDEX IF NOT EXISTS idx_problems_priority ON unsolved_problems(priority); - `); - - // Insights discovered by Shadow - db.exec(` - CREATE TABLE IF NOT EXISTS shadow_insights ( - id TEXT PRIMARY KEY, - problem_id TEXT NOT NULL, - insight TEXT NOT NULL, - source TEXT NOT NULL, - confidence REAL DEFAULT 0.5, - related_node_ids TEXT DEFAULT '[]', - created_at TEXT NOT NULL, - - FOREIGN KEY (problem_id) REFERENCES unsolved_problems(id) ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS idx_insights_problem ON shadow_insights(problem_id); - `); - - return db; -} - -// ============================================================================ -// SHADOW SELF CLASS -// ============================================================================ - -export class ShadowSelf { - private db: Database.Database; - - constructor() { - this.db = initializeShadowDb(); - } - - /** - * Log a new unsolved problem - */ - logProblem(description: string, options: { - context?: string; - tags?: string[]; - priority?: number; - } = {}): UnsolvedProblem { - const id = nanoid(); - const now = new Date().toISOString(); - - const stmt = this.db.prepare(` - INSERT INTO unsolved_problems ( - id, description, context, tags, status, priority, - attempts, last_attempt_at, created_at, updated_at, - solution, related_node_ids - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, - description, - options.context || '', - JSON.stringify(options.tags || []), - 'open', - options.priority || 3, - 0, - null, - now, - now, - null, - '[]' - ); - - return this.getProblem(id)!; - } - - /** - * Get a specific problem - */ - getProblem(id: string): UnsolvedProblem | null { - const stmt = this.db.prepare('SELECT * FROM unsolved_problems WHERE id = ?'); - const row = stmt.get(id) as Record | undefined; - if (!row) return null; - return this.rowToProblem(row); - } - - /** - * Get all open problems - */ - getOpenProblems(): UnsolvedProblem[] { - const stmt = this.db.prepare(` - SELECT * FROM unsolved_problems - WHERE status IN ('open', 'investigating') - ORDER BY priority DESC, created_at ASC - `); - const rows = stmt.all() as Record[]; - return rows.map(row => this.rowToProblem(row)); - } - - /** - * Update problem status - */ - updateStatus(id: string, status: UnsolvedProblem['status'], solution?: string): void { - const now = new Date().toISOString(); - const stmt = this.db.prepare(` - UPDATE unsolved_problems - SET status = ?, solution = ?, updated_at = ? - WHERE id = ? - `); - stmt.run(status, solution || null, now, id); - } - - /** - * Mark problem as solved - */ - markSolved(id: string, solution: string): void { - this.updateStatus(id, 'solved', solution); - } - - /** - * Add insight to a problem - */ - addInsight(problemId: string, insight: string, options: { - source?: ShadowInsight['source']; - confidence?: number; - relatedNodeIds?: string[]; - } = {}): ShadowInsight { - const id = nanoid(); - const now = new Date().toISOString(); - - const stmt = this.db.prepare(` - INSERT INTO shadow_insights ( - id, problem_id, insight, source, confidence, related_node_ids, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, - problemId, - insight, - options.source || 'keyword_match', - options.confidence || 0.5, - JSON.stringify(options.relatedNodeIds || []), - now - ); - - // Update problem attempt count - this.db.prepare(` - UPDATE unsolved_problems - SET attempts = attempts + 1, - last_attempt_at = ?, - status = 'investigating', - updated_at = ? - WHERE id = ? - `).run(now, now, problemId); - - return { - id, - problemId, - insight, - source: options.source || 'keyword_match', - confidence: options.confidence || 0.5, - relatedNodeIds: options.relatedNodeIds || [], - createdAt: new Date(now), - }; - } - - /** - * Get insights for a problem - */ - getInsights(problemId: string): ShadowInsight[] { - const stmt = this.db.prepare(` - SELECT * FROM shadow_insights - WHERE problem_id = ? - ORDER BY created_at DESC - `); - const rows = stmt.all(problemId) as Record[]; - - return rows.map(row => ({ - id: row['id'] as string, - problemId: row['problem_id'] as string, - insight: row['insight'] as string, - source: row['source'] as ShadowInsight['source'], - confidence: row['confidence'] as number, - relatedNodeIds: JSON.parse(row['related_node_ids'] as string || '[]'), - createdAt: new Date(row['created_at'] as string), - })); - } - - /** - * Get problems that haven't been worked on recently - */ - getStaleProblems(hoursSinceLastAttempt: number = 24): UnsolvedProblem[] { - const cutoff = new Date(Date.now() - hoursSinceLastAttempt * 60 * 60 * 1000); - - const stmt = this.db.prepare(` - SELECT * FROM unsolved_problems - WHERE status IN ('open', 'investigating') - AND (last_attempt_at IS NULL OR last_attempt_at < ?) - ORDER BY priority DESC - `); - const rows = stmt.all(cutoff.toISOString()) as Record[]; - return rows.map(row => this.rowToProblem(row)); - } - - /** - * Get statistics - */ - getStats(): { - total: number; - open: number; - investigating: number; - solved: number; - abandoned: number; - totalInsights: number; - } { - const statusCounts = this.db.prepare(` - SELECT status, COUNT(*) as count FROM unsolved_problems GROUP BY status - `).all() as { status: string; count: number }[]; - - const insightCount = this.db.prepare(` - SELECT COUNT(*) as count FROM shadow_insights - `).get() as { count: number }; - - const stats = { - total: 0, - open: 0, - investigating: 0, - solved: 0, - abandoned: 0, - totalInsights: insightCount.count, - }; - - for (const { status, count } of statusCounts) { - stats.total += count; - if (status === 'open') stats.open = count; - if (status === 'investigating') stats.investigating = count; - if (status === 'solved') stats.solved = count; - if (status === 'abandoned') stats.abandoned = count; - } - - return stats; - } - - private rowToProblem(row: Record): UnsolvedProblem { - return { - id: row['id'] as string, - description: row['description'] as string, - context: row['context'] as string, - tags: JSON.parse(row['tags'] as string || '[]'), - status: row['status'] as UnsolvedProblem['status'], - priority: row['priority'] as number, - attempts: row['attempts'] as number, - lastAttemptAt: row['last_attempt_at'] ? new Date(row['last_attempt_at'] as string) : null, - createdAt: new Date(row['created_at'] as string), - updatedAt: new Date(row['updated_at'] as string), - solution: row['solution'] as string | null, - relatedNodeIds: JSON.parse(row['related_node_ids'] as string || '[]'), - }; - } - - close(): void { - this.db.close(); - } -} - -// ============================================================================ -// SHADOW WORK - Background processing -// ============================================================================ - -import { VestigeDatabase } from './database.js'; - -/** - * Run Shadow work cycle - look for new insights on unsolved problems - */ -export function runShadowCycle(shadow: ShadowSelf, vestige: VestigeDatabase): { - problemsAnalyzed: number; - insightsGenerated: number; - insights: Array<{ problem: string; insight: string }>; -} { - const result = { - problemsAnalyzed: 0, - insightsGenerated: 0, - insights: [] as Array<{ problem: string; insight: string }>, - }; - - // Get stale problems that need attention - const problems = shadow.getStaleProblems(1); // Haven't been worked on in 1 hour - - for (const problem of problems) { - result.problemsAnalyzed++; - - // Extract keywords from problem description - const keywords = problem.description - .toLowerCase() - .split(/\W+/) - .filter(w => w.length > 4); - - // Search knowledge base for related content - for (const keyword of keywords.slice(0, 5)) { - try { - const searchResult = vestige.searchNodes(keyword, { limit: 3 }); - - for (const node of searchResult.items) { - // Check if this node was added after the problem - if (node.createdAt > problem.createdAt) { - // New knowledge! This might help - shadow.addInsight(problem.id, `New knowledge found: "${node.content.slice(0, 100)}..."`, { - source: 'new_knowledge', - confidence: 0.6, - relatedNodeIds: [node.id], - }); - - result.insightsGenerated++; - result.insights.push({ - problem: problem.description.slice(0, 50), - insight: `Found related: ${node.content.slice(0, 50)}...`, - }); - } - } - } catch { - // Ignore search errors - } - } - } - - return result; -} diff --git a/packages/core/src/core/types.ts b/packages/core/src/core/types.ts deleted file mode 100644 index f85f255..0000000 --- a/packages/core/src/core/types.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { z } from 'zod'; - -// ============================================================================ -// SOURCE TYPES -// ============================================================================ - -export const SourceTypeSchema = z.enum([ - 'note', - 'conversation', - 'email', - 'book', - 'article', - 'highlight', - 'meeting', - 'manual', - 'webpage', -]); -export type SourceType = z.infer; - -export const SourcePlatformSchema = z.enum([ - 'obsidian', - 'notion', - 'roam', - 'logseq', - 'claude', - 'chatgpt', - 'gmail', - 'outlook', - 'kindle', - 'readwise', - 'pocket', - 'instapaper', - 'manual', - 'browser', -]); -export type SourcePlatform = z.infer; - -// ============================================================================ -// KNOWLEDGE NODE -// ============================================================================ - -export const KnowledgeNodeSchema = z.object({ - id: z.string(), - content: z.string(), - summary: z.string().optional(), - - // Temporal metadata - createdAt: z.date(), - updatedAt: z.date(), - lastAccessedAt: z.date(), - accessCount: z.number().default(0), - - // Decay modeling (SM-2 inspired spaced repetition) - retentionStrength: z.number().min(0).max(1).default(1), - stabilityFactor: z.number().min(1).optional().default(1), // Grows with reviews, flattens decay curve - sentimentIntensity: z.number().min(0).max(1).optional().default(0), // Emotional weight - higher = decays slower - nextReviewDate: z.date().optional(), - reviewCount: z.number().default(0), - - // Dual-Strength Memory Model (Bjork & Bjork, 1992) - storageStrength: z.number().min(1).default(1), // How well encoded (never decreases) - retrievalStrength: z.number().min(0).max(1).default(1), // How accessible now (decays) - - // Provenance - sourceType: SourceTypeSchema, - sourcePlatform: SourcePlatformSchema, - sourceId: z.string().optional(), // Original source reference - sourceUrl: z.string().optional(), - sourceChain: z.array(z.string()).default([]), // Full provenance path - - // Git-Blame for Thoughts - what code was being worked on when this memory was created? - gitContext: z.object({ - branch: z.string().optional(), - commit: z.string().optional(), // Short SHA - commitMessage: z.string().optional(), // First line of commit message - repoPath: z.string().optional(), // Repository root path - dirty: z.boolean().optional(), // Had uncommitted changes? - changedFiles: z.array(z.string()).optional(), // Files with uncommitted changes - }).optional(), - - // Confidence & quality - confidence: z.number().min(0).max(1).default(0.8), - isContradicted: z.boolean().default(false), - contradictionIds: z.array(z.string()).default([]), - - // Extracted entities - people: z.array(z.string()).default([]), - concepts: z.array(z.string()).default([]), - events: z.array(z.string()).default([]), - tags: z.array(z.string()).default([]), -}); -export type KnowledgeNode = z.infer; -// Input type where optional/default fields are truly optional (for insertNode) -export type KnowledgeNodeInput = z.input; - -// ============================================================================ -// PERSON NODE (People Memory / Mini-CRM) -// ============================================================================ - -export const InteractionTypeSchema = z.enum([ - 'meeting', - 'email', - 'call', - 'message', - 'social', - 'collaboration', - 'mention', // Referenced in notes but not direct interaction -]); -export type InteractionType = z.infer; - -export const InteractionSchema = z.object({ - id: z.string(), - personId: z.string(), - type: InteractionTypeSchema, - date: z.date(), - summary: z.string(), - topics: z.array(z.string()).default([]), - sentiment: z.number().min(-1).max(1).optional(), // -1 negative, 0 neutral, 1 positive - actionItems: z.array(z.string()).default([]), - sourceNodeId: z.string().optional(), // Link to knowledge node if derived -}); -export type Interaction = z.infer; - -export const PersonNodeSchema = z.object({ - id: z.string(), - name: z.string(), - aliases: z.array(z.string()).default([]), - - // Relationship context - howWeMet: z.string().optional(), - relationshipType: z.string().optional(), // colleague, friend, mentor, family, etc. - organization: z.string().optional(), - role: z.string().optional(), - location: z.string().optional(), - - // Contact info - email: z.string().optional(), - phone: z.string().optional(), - socialLinks: z.record(z.string()).default({}), - - // Communication patterns - lastContactAt: z.date().optional(), - contactFrequency: z.number().default(0), // Interactions per month (calculated) - preferredChannel: z.string().optional(), - - // Shared context - sharedTopics: z.array(z.string()).default([]), - sharedProjects: z.array(z.string()).default([]), - - // Meta - notes: z.string().optional(), - relationshipHealth: z.number().min(0).max(1).default(0.5), // Calculated from recency + frequency - - createdAt: z.date(), - updatedAt: z.date(), -}); -export type PersonNode = z.infer; - -// ============================================================================ -// GRAPH EDGES (Relationships) -// ============================================================================ - -export const EdgeTypeSchema = z.enum([ - 'relates_to', - 'derived_from', - 'contradicts', - 'supports', - 'references', - 'part_of', - 'follows', // Temporal sequence - 'person_mentioned', - 'concept_instance', - 'similar_to', -]); -export type EdgeType = z.infer; - -export const GraphEdgeSchema = z.object({ - id: z.string(), - fromId: z.string(), - toId: z.string(), - edgeType: EdgeTypeSchema, - weight: z.number().min(0).max(1).default(0.5), - metadata: z.record(z.unknown()).default({}), - createdAt: z.date(), -}); -export type GraphEdge = z.infer; - -// ============================================================================ -// SOURCE TRACKING -// ============================================================================ - -export const SourceSchema = z.object({ - id: z.string(), - type: SourceTypeSchema, - platform: SourcePlatformSchema, - originalId: z.string().optional(), - url: z.string().optional(), - filePath: z.string().optional(), - title: z.string().optional(), - author: z.string().optional(), - publicationDate: z.date().optional(), - - // Sync tracking - ingestedAt: z.date(), - lastSyncedAt: z.date(), - contentHash: z.string().optional(), // For change detection - - // Stats - nodeCount: z.number().default(0), -}); -export type Source = z.infer; - -// ============================================================================ -// TOOL INPUT/OUTPUT SCHEMAS -// ============================================================================ - -export const IngestInputSchema = z.object({ - content: z.string(), - source: SourceTypeSchema.optional().default('manual'), - platform: SourcePlatformSchema.optional().default('manual'), - sourceId: z.string().optional(), - sourceUrl: z.string().optional(), - timestamp: z.string().datetime().optional(), - people: z.array(z.string()).optional(), - tags: z.array(z.string()).optional(), - title: z.string().optional(), -}); -export type IngestInput = z.infer; - -export const RecallOptionsSchema = z.object({ - query: z.string(), - timeRange: z.object({ - start: z.string().datetime().optional(), - end: z.string().datetime().optional(), - }).optional(), - sources: z.array(SourceTypeSchema).optional(), - platforms: z.array(SourcePlatformSchema).optional(), - people: z.array(z.string()).optional(), - minConfidence: z.number().min(0).max(1).optional(), - limit: z.number().min(1).max(100).optional().default(10), - includeContext: z.boolean().optional().default(true), -}); -export type RecallOptions = z.infer; - -export const RecallResultSchema = z.object({ - node: KnowledgeNodeSchema, - score: z.number(), - matchType: z.enum(['semantic', 'keyword', 'graph']), - context: z.string().optional(), - relatedNodes: z.array(z.string()).optional(), -}); -export type RecallResult = z.infer; - -export const SynthesisOptionsSchema = z.object({ - topic: z.string(), - depth: z.enum(['shallow', 'deep']).optional().default('shallow'), - format: z.enum(['summary', 'outline', 'narrative']).optional().default('summary'), - maxSources: z.number().optional().default(20), -}); -export type SynthesisOptions = z.infer; - -// ============================================================================ -// DECAY MODELING -// ============================================================================ - -export interface DecayConfig { - // Ebbinghaus forgetting curve parameters - initialRetention: number; // Starting retention (default 1.0) - decayRate: number; // Base decay rate (default ~0.9 for typical forgetting) - minRetention: number; // Floor retention (default 0.1) - reviewBoost: number; // How much review increases retention (default 0.3) - accessBoost: number; // How much access slows decay (default 0.1) -} - -export const DEFAULT_DECAY_CONFIG: DecayConfig = { - initialRetention: 1.0, - decayRate: 0.9, - minRetention: 0.1, - reviewBoost: 0.3, - accessBoost: 0.1, -}; - -// ============================================================================ -// DAILY BRIEF -// ============================================================================ - -export const DailyBriefSchema = z.object({ - date: z.date(), - stats: z.object({ - totalNodes: z.number(), - addedToday: z.number(), - addedThisWeek: z.number(), - connectionsDiscovered: z.number(), - }), - reviewDue: z.array(z.object({ - nodeId: z.string(), - summary: z.string(), - lastAccessed: z.date(), - retentionStrength: z.number(), - })), - peopleToReconnect: z.array(z.object({ - personId: z.string(), - name: z.string(), - daysSinceContact: z.number(), - sharedTopics: z.array(z.string()), - })), - interestingConnections: z.array(z.object({ - nodeA: z.string(), - nodeB: z.string(), - connectionReason: z.string(), - })), - recentThemes: z.array(z.string()), -}); -export type DailyBrief = z.infer; diff --git a/packages/core/src/core/vector-store.ts b/packages/core/src/core/vector-store.ts deleted file mode 100644 index 30bbe5b..0000000 --- a/packages/core/src/core/vector-store.ts +++ /dev/null @@ -1,1154 +0,0 @@ -/** - * Vector Store Integration for Vestige MCP - * - * Provides semantic search capabilities via vector embeddings. - * Primary: ChromaDB (when available) - fast, efficient vector database - * Fallback: SQLite (embedded) - works offline, no external dependencies - * - * Design Philosophy: - * - Graceful degradation: Works without ChromaDB, just slower - * - Zero configuration: Auto-detects available backends - * - Production-ready: Full error handling, logging, retry logic - */ - -import type Database from 'better-sqlite3'; - -// ============================================================================ -// CONFIGURATION -// ============================================================================ - -const CHROMA_HOST = process.env['CHROMA_HOST'] ?? 'http://localhost:8000'; -const COLLECTION_NAME = 'vestige_embeddings'; -const DEFAULT_SIMILARITY_LIMIT = 10; -const MAX_SIMILARITY_LIMIT = 100; - -// Connection settings -const CONNECTION_TIMEOUT_MS = 5000; -const MAX_RETRIES = 3; -const RETRY_DELAY_MS = 1000; - -// Batch settings for bulk operations -const BATCH_SIZE = 100; - -// ============================================================================ -// ERROR TYPES -// ============================================================================ - -export class VectorStoreError extends Error { - constructor( - message: string, - public readonly code: string, - public readonly isRetryable: boolean = false, - cause?: unknown - ) { - super(message); - this.name = 'VectorStoreError'; - if (cause) { - this.cause = cause; - } - } -} - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface SimilarityResult { - id: string; - similarity: number; - content?: string | undefined; - metadata?: Record | undefined; -} - -export interface VectorStoreStats { - backend: 'chromadb' | 'sqlite'; - embeddingCount: number; - collectionName?: string; - isAvailable: boolean; -} - -export interface IVectorStore { - initialize(): Promise; - isAvailable(): Promise; - upsertEmbedding( - nodeId: string, - embedding: number[], - content: string, - metadata?: Record - ): Promise; - findSimilar( - embedding: number[], - limit?: number, - filter?: Record - ): Promise; - deleteEmbedding(nodeId: string): Promise; - getEmbedding(nodeId: string): Promise; - getStats(): Promise; - close(): Promise; -} - -// ============================================================================ -// UTILITY FUNCTIONS -// ============================================================================ - -/** - * Calculate cosine similarity between two vectors - * Returns value between -1 and 1 (1 = identical, 0 = orthogonal, -1 = opposite) - */ -function cosineSimilarity(a: number[], b: number[]): number { - if (a.length !== b.length) { - throw new VectorStoreError( - `Vector dimension mismatch: ${a.length} vs ${b.length}`, - 'DIMENSION_MISMATCH' - ); - } - - let dotProduct = 0; - let normA = 0; - let normB = 0; - - for (let i = 0; i < a.length; i++) { - const aVal = a[i] ?? 0; - const bVal = b[i] ?? 0; - dotProduct += aVal * bVal; - normA += aVal * aVal; - normB += bVal * bVal; - } - - const magnitude = Math.sqrt(normA) * Math.sqrt(normB); - if (magnitude === 0) return 0; - - return dotProduct / magnitude; -} - -/** - * Convert ChromaDB distance to similarity score - * ChromaDB uses L2 (euclidean) distance by default, lower = more similar - * We convert to similarity: 1 / (1 + distance) - */ -function distanceToSimilarity(distance: number): number { - return 1 / (1 + distance); -} - -/** - * Sleep utility for retry delays - */ -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Retry wrapper for operations that may fail transiently - */ -async function withRetry( - operation: () => Promise, - maxRetries: number = MAX_RETRIES, - delayMs: number = RETRY_DELAY_MS -): Promise { - let lastError: unknown; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - return await operation(); - } catch (error) { - lastError = error; - - // Check if error is retryable - if (error instanceof VectorStoreError && !error.isRetryable) { - throw error; - } - - if (attempt < maxRetries) { - await sleep(delayMs * attempt); // Exponential backoff - } - } - } - - throw lastError; -} - -// ============================================================================ -// CHROMADB VECTOR STORE -// ============================================================================ - -/** - * ChromaDB-backed vector store for fast semantic search - * - * Features: - * - Persistent storage via ChromaDB server - * - Fast approximate nearest neighbor search - * - Metadata filtering support - * - Automatic retry on transient failures - */ -export class ChromaVectorStore implements IVectorStore { - private client: import('chromadb').ChromaClient | null = null; - private collection: import('chromadb').Collection | null = null; - private available: boolean | null = null; - private initPromise: Promise | null = null; - - constructor(private readonly host: string = CHROMA_HOST) {} - - /** - * Initialize connection to ChromaDB - * Creates collection if it doesn't exist - */ - async initialize(): Promise { - // Dedupe concurrent initialization calls - if (this.initPromise) { - return this.initPromise; - } - - this.initPromise = this.doInitialize(); - return this.initPromise; - } - - private async doInitialize(): Promise { - try { - // Dynamic import to avoid hard dependency - const { ChromaClient } = await import('chromadb'); - - this.client = new ChromaClient({ path: this.host }); - - // Test connection with timeout - const timeoutPromise = new Promise((_, reject) => { - setTimeout( - () => reject(new Error('Connection timeout')), - CONNECTION_TIMEOUT_MS - ); - }); - - await Promise.race([this.client.heartbeat(), timeoutPromise]); - - // Get or create collection - this.collection = await this.client.getOrCreateCollection({ - name: COLLECTION_NAME, - metadata: { - 'hnsw:space': 'cosine', // Use cosine similarity - description: 'Vestige knowledge node embeddings', - }, - }); - - this.available = true; - console.log( - `[VectorStore] ChromaDB connected at ${this.host}, collection: ${COLLECTION_NAME}` - ); - - return true; - } catch (error) { - this.available = false; - console.warn( - `[VectorStore] ChromaDB not available at ${this.host}:`, - error instanceof Error ? error.message : 'Unknown error' - ); - return false; - } - } - - /** - * Check if ChromaDB is currently available - */ - async isAvailable(): Promise { - if (this.available === null) { - await this.initialize(); - } - - // Re-check heartbeat for ongoing availability - if (this.available && this.client) { - try { - await Promise.race([ - this.client.heartbeat(), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Heartbeat timeout')), 2000) - ), - ]); - return true; - } catch { - this.available = false; - return false; - } - } - - return this.available ?? false; - } - - /** - * Add or update an embedding in ChromaDB - */ - async upsertEmbedding( - nodeId: string, - embedding: number[], - content: string, - metadata?: Record - ): Promise { - if (!this.collection) { - throw new VectorStoreError( - 'ChromaDB not initialized', - 'NOT_INITIALIZED' - ); - } - - // Validate embedding - if (!Array.isArray(embedding) || embedding.length === 0) { - throw new VectorStoreError( - 'Invalid embedding: must be non-empty array', - 'INVALID_EMBEDDING' - ); - } - - // Sanitize metadata - ChromaDB only accepts primitive values - const sanitizedMetadata: Record = {}; - if (metadata) { - for (const [key, value] of Object.entries(metadata)) { - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - sanitizedMetadata[key] = value; - } else if (value !== null && value !== undefined) { - // Convert complex types to JSON strings - sanitizedMetadata[key] = JSON.stringify(value); - } - } - } - - try { - await withRetry(async () => { - await this.collection!.upsert({ - ids: [nodeId], - embeddings: [embedding], - documents: [content], - metadatas: [sanitizedMetadata], - }); - }); - } catch (error) { - throw new VectorStoreError( - `Failed to upsert embedding for ${nodeId}`, - 'UPSERT_FAILED', - true, - error - ); - } - } - - /** - * Find similar embeddings using vector similarity search - */ - async findSimilar( - embedding: number[], - limit: number = DEFAULT_SIMILARITY_LIMIT, - filter?: Record - ): Promise { - if (!this.collection) { - throw new VectorStoreError( - 'ChromaDB not initialized', - 'NOT_INITIALIZED' - ); - } - - const safeLimit = Math.min(Math.max(1, limit), MAX_SIMILARITY_LIMIT); - - // Convert filter to ChromaDB where clause format - let whereClause: Record | undefined; - if (filter && Object.keys(filter).length > 0) { - whereClause = {}; - for (const [key, value] of Object.entries(filter)) { - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - whereClause[key] = value; - } - } - } - - try { - const results = await withRetry(async () => { - // Note: ChromaDB IncludeEnum values need to be typed correctly - // Using type assertion since the SDK types may be stricter than needed - return this.collection!.query({ - queryEmbeddings: [embedding], - nResults: safeLimit, - where: whereClause, - include: ['documents', 'metadatas', 'distances'] as const, - } as Parameters[0]); - }); - - // Transform results - const similarResults: SimilarityResult[] = []; - - if (results.ids[0]) { - for (let i = 0; i < results.ids[0].length; i++) { - const id = results.ids[0][i]; - if (!id) continue; - - const distance = results.distances?.[0]?.[i] ?? 0; - const document = results.documents?.[0]?.[i]; - const metadata = results.metadatas?.[0]?.[i]; - - similarResults.push({ - id, - similarity: distanceToSimilarity(distance), - content: document ?? undefined, - metadata: metadata as Record | undefined, - }); - } - } - - return similarResults; - } catch (error) { - throw new VectorStoreError( - 'Failed to query similar embeddings', - 'QUERY_FAILED', - true, - error - ); - } - } - - /** - * Delete an embedding from ChromaDB - */ - async deleteEmbedding(nodeId: string): Promise { - if (!this.collection) { - throw new VectorStoreError( - 'ChromaDB not initialized', - 'NOT_INITIALIZED' - ); - } - - try { - await withRetry(async () => { - await this.collection!.delete({ ids: [nodeId] }); - }); - } catch (error) { - throw new VectorStoreError( - `Failed to delete embedding for ${nodeId}`, - 'DELETE_FAILED', - true, - error - ); - } - } - - /** - * Get embedding for a specific node - */ - async getEmbedding(nodeId: string): Promise { - if (!this.collection) { - throw new VectorStoreError( - 'ChromaDB not initialized', - 'NOT_INITIALIZED' - ); - } - - try { - const result = await this.collection.get({ - ids: [nodeId], - include: ['embeddings'] as const, - } as Parameters[0]); - - if (result.embeddings && result.embeddings[0]) { - return result.embeddings[0] as number[]; - } - - return null; - } catch (error) { - throw new VectorStoreError( - `Failed to get embedding for ${nodeId}`, - 'GET_FAILED', - true, - error - ); - } - } - - /** - * Get statistics about the vector store - */ - async getStats(): Promise { - const isAvailable = await this.isAvailable(); - - if (!isAvailable || !this.collection) { - return { - backend: 'chromadb', - embeddingCount: 0, - collectionName: COLLECTION_NAME, - isAvailable: false, - }; - } - - try { - const count = await this.collection.count(); - return { - backend: 'chromadb', - embeddingCount: count, - collectionName: COLLECTION_NAME, - isAvailable: true, - }; - } catch { - return { - backend: 'chromadb', - embeddingCount: 0, - collectionName: COLLECTION_NAME, - isAvailable: false, - }; - } - } - - /** - * Close the ChromaDB connection - */ - async close(): Promise { - // ChromaDB client doesn't need explicit closing - this.client = null; - this.collection = null; - this.available = null; - this.initPromise = null; - } -} - -// ============================================================================ -// SQLITE VECTOR STORE (FALLBACK) -// ============================================================================ - -/** - * SQLite-backed vector store for offline/embedded use - * - * Stores embeddings as JSON in SQLite when ChromaDB is unavailable. - * Slower than ChromaDB but works without external dependencies. - * - * Features: - * - Zero external dependencies - * - Works offline - * - Brute-force cosine similarity (O(n) per query) - * - Good enough for small-medium datasets (<10k embeddings) - */ -export class SQLiteVectorStore implements IVectorStore { - private db: Database.Database | null = null; - private initialized = false; - - constructor(private readonly getDatabase: () => Database.Database) {} - - /** - * Initialize SQLite vector store - * Creates embeddings_local table if needed - */ - async initialize(): Promise { - try { - this.db = this.getDatabase(); - - // Create table for storing embeddings locally - this.db.exec(` - CREATE TABLE IF NOT EXISTS embeddings_local ( - node_id TEXT PRIMARY KEY, - embedding TEXT NOT NULL, - content TEXT, - metadata TEXT, - dimension INTEGER NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_embeddings_local_dimension - ON embeddings_local(dimension); - `); - - this.initialized = true; - console.log('[VectorStore] SQLite fallback initialized'); - - return true; - } catch (error) { - console.error('[VectorStore] SQLite initialization failed:', error); - return false; - } - } - - /** - * SQLite fallback is always available if initialized - */ - async isAvailable(): Promise { - return this.initialized && this.db !== null; - } - - /** - * Store embedding in SQLite as JSON - */ - async upsertEmbedding( - nodeId: string, - embedding: number[], - content: string, - metadata?: Record - ): Promise { - if (!this.db || !this.initialized) { - throw new VectorStoreError( - 'SQLite vector store not initialized', - 'NOT_INITIALIZED' - ); - } - - // Validate embedding - if (!Array.isArray(embedding) || embedding.length === 0) { - throw new VectorStoreError( - 'Invalid embedding: must be non-empty array', - 'INVALID_EMBEDDING' - ); - } - - const now = new Date().toISOString(); - - try { - const stmt = this.db.prepare(` - INSERT INTO embeddings_local ( - node_id, embedding, content, metadata, dimension, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(node_id) DO UPDATE SET - embedding = excluded.embedding, - content = excluded.content, - metadata = excluded.metadata, - dimension = excluded.dimension, - updated_at = excluded.updated_at - `); - - stmt.run( - nodeId, - JSON.stringify(embedding), - content, - metadata ? JSON.stringify(metadata) : null, - embedding.length, - now, - now - ); - } catch (error) { - throw new VectorStoreError( - `Failed to upsert embedding for ${nodeId}`, - 'UPSERT_FAILED', - false, - error - ); - } - } - - /** - * Find similar embeddings using brute-force cosine similarity - * - * NOTE: This is O(n) - suitable for small datasets only. - * For large datasets, use ChromaDB instead. - */ - async findSimilar( - embedding: number[], - limit: number = DEFAULT_SIMILARITY_LIMIT, - filter?: Record - ): Promise { - if (!this.db || !this.initialized) { - throw new VectorStoreError( - 'SQLite vector store not initialized', - 'NOT_INITIALIZED' - ); - } - - const safeLimit = Math.min(Math.max(1, limit), MAX_SIMILARITY_LIMIT); - - try { - // First, filter by dimension to avoid comparing incompatible vectors - const stmt = this.db.prepare(` - SELECT node_id, embedding, content, metadata - FROM embeddings_local - WHERE dimension = ? - `); - - type EmbeddingRow = { - node_id: string; - embedding: string; - content: string | null; - metadata: string | null; - }; - - const rows = stmt.all(embedding.length) as EmbeddingRow[]; - - // Calculate similarity for each embedding - const results: SimilarityResult[] = []; - - for (const row of rows) { - let storedEmbedding: number[]; - try { - storedEmbedding = JSON.parse(row.embedding) as number[]; - } catch { - continue; // Skip corrupted embeddings - } - - // Apply metadata filter if provided - if (filter && Object.keys(filter).length > 0) { - let rowMetadata: Record = {}; - if (row.metadata) { - try { - rowMetadata = JSON.parse(row.metadata) as Record; - } catch { - // Invalid metadata, skip filter - } - } - - let matches = true; - for (const [key, value] of Object.entries(filter)) { - if (rowMetadata[key] !== value) { - matches = false; - break; - } - } - - if (!matches) continue; - } - - const similarity = cosineSimilarity(embedding, storedEmbedding); - - let metadata: Record | undefined; - if (row.metadata) { - try { - metadata = JSON.parse(row.metadata) as Record; - } catch { - // Ignore invalid metadata - } - } - - results.push({ - id: row.node_id, - similarity, - content: row.content ?? undefined, - metadata, - }); - } - - // Sort by similarity descending and take top N - results.sort((a, b) => b.similarity - a.similarity); - - return results.slice(0, safeLimit); - } catch (error) { - if (error instanceof VectorStoreError) throw error; - throw new VectorStoreError( - 'Failed to query similar embeddings', - 'QUERY_FAILED', - false, - error - ); - } - } - - /** - * Delete an embedding from SQLite - */ - async deleteEmbedding(nodeId: string): Promise { - if (!this.db || !this.initialized) { - throw new VectorStoreError( - 'SQLite vector store not initialized', - 'NOT_INITIALIZED' - ); - } - - try { - const stmt = this.db.prepare( - 'DELETE FROM embeddings_local WHERE node_id = ?' - ); - stmt.run(nodeId); - } catch (error) { - throw new VectorStoreError( - `Failed to delete embedding for ${nodeId}`, - 'DELETE_FAILED', - false, - error - ); - } - } - - /** - * Get embedding for a specific node - */ - async getEmbedding(nodeId: string): Promise { - if (!this.db || !this.initialized) { - throw new VectorStoreError( - 'SQLite vector store not initialized', - 'NOT_INITIALIZED' - ); - } - - try { - const stmt = this.db.prepare( - 'SELECT embedding FROM embeddings_local WHERE node_id = ?' - ); - const row = stmt.get(nodeId) as { embedding: string } | undefined; - - if (!row) return null; - - return JSON.parse(row.embedding) as number[]; - } catch (error) { - throw new VectorStoreError( - `Failed to get embedding for ${nodeId}`, - 'GET_FAILED', - false, - error - ); - } - } - - /** - * Get statistics about the SQLite vector store - */ - async getStats(): Promise { - if (!this.db || !this.initialized) { - return { - backend: 'sqlite', - embeddingCount: 0, - isAvailable: false, - }; - } - - try { - const row = this.db - .prepare('SELECT COUNT(*) as count FROM embeddings_local') - .get() as { count: number }; - - return { - backend: 'sqlite', - embeddingCount: row.count, - isAvailable: true, - }; - } catch { - return { - backend: 'sqlite', - embeddingCount: 0, - isAvailable: false, - }; - } - } - - /** - * Close the SQLite vector store - */ - async close(): Promise { - // Don't close the shared database connection - // Just clear our reference - this.db = null; - this.initialized = false; - } -} - -// ============================================================================ -// HYBRID VECTOR STORE -// ============================================================================ - -/** - * Hybrid vector store that combines ChromaDB and SQLite - * - * Strategy: - * - Try ChromaDB first (fast, scalable) - * - Fall back to SQLite if unavailable (offline, embedded) - * - Sync between stores when ChromaDB becomes available - */ -export class HybridVectorStore implements IVectorStore { - private chromaStore: ChromaVectorStore; - private sqliteStore: SQLiteVectorStore; - private activeStore: IVectorStore | null = null; - - constructor(getDatabase: () => Database.Database, chromaHost?: string) { - this.chromaStore = new ChromaVectorStore(chromaHost); - this.sqliteStore = new SQLiteVectorStore(getDatabase); - } - - /** - * Initialize both stores and select the best available - */ - async initialize(): Promise { - // Try ChromaDB first - const chromaAvailable = await this.chromaStore.initialize(); - - if (chromaAvailable) { - this.activeStore = this.chromaStore; - console.log('[VectorStore] Using ChromaDB backend'); - return true; - } - - // Fall back to SQLite - const sqliteAvailable = await this.sqliteStore.initialize(); - - if (sqliteAvailable) { - this.activeStore = this.sqliteStore; - console.log('[VectorStore] Using SQLite fallback backend'); - return true; - } - - console.error('[VectorStore] No backend available'); - return false; - } - - async isAvailable(): Promise { - if (!this.activeStore) return false; - return this.activeStore.isAvailable(); - } - - async upsertEmbedding( - nodeId: string, - embedding: number[], - content: string, - metadata?: Record - ): Promise { - if (!this.activeStore) { - throw new VectorStoreError('No vector store available', 'NOT_INITIALIZED'); - } - - await this.activeStore.upsertEmbedding(nodeId, embedding, content, metadata); - } - - async findSimilar( - embedding: number[], - limit?: number, - filter?: Record - ): Promise { - if (!this.activeStore) { - throw new VectorStoreError('No vector store available', 'NOT_INITIALIZED'); - } - - return this.activeStore.findSimilar(embedding, limit, filter); - } - - async deleteEmbedding(nodeId: string): Promise { - if (!this.activeStore) { - throw new VectorStoreError('No vector store available', 'NOT_INITIALIZED'); - } - - await this.activeStore.deleteEmbedding(nodeId); - } - - async getEmbedding(nodeId: string): Promise { - if (!this.activeStore) { - throw new VectorStoreError('No vector store available', 'NOT_INITIALIZED'); - } - - return this.activeStore.getEmbedding(nodeId); - } - - async getStats(): Promise { - if (!this.activeStore) { - return { - backend: 'sqlite', - embeddingCount: 0, - isAvailable: false, - }; - } - - return this.activeStore.getStats(); - } - - /** - * Get the currently active backend type - */ - getActiveBackend(): 'chromadb' | 'sqlite' | null { - if (this.activeStore === this.chromaStore) return 'chromadb'; - if (this.activeStore === this.sqliteStore) return 'sqlite'; - return null; - } - - /** - * Attempt to switch to ChromaDB if it becomes available - */ - async tryUpgradeToChroma(): Promise { - if (this.activeStore === this.chromaStore) { - return true; // Already using ChromaDB - } - - const chromaAvailable = await this.chromaStore.isAvailable(); - if (chromaAvailable) { - this.activeStore = this.chromaStore; - console.log('[VectorStore] Upgraded to ChromaDB backend'); - return true; - } - - return false; - } - - async close(): Promise { - await this.chromaStore.close(); - await this.sqliteStore.close(); - this.activeStore = null; - } -} - -// ============================================================================ -// FACTORY FUNCTION -// ============================================================================ - -/** - * Create and initialize the appropriate vector store - * - * Tries ChromaDB first, falls back to SQLite if unavailable. - * - * Usage: - * ```typescript - * const vectorStore = await createVectorStore(db); - * await vectorStore.upsertEmbedding('node-1', embedding, 'content'); - * const similar = await vectorStore.findSimilar(queryEmbedding, 10); - * ``` - */ -export async function createVectorStore( - getDatabase: () => Database.Database, - chromaHost?: string -): Promise { - const store = new HybridVectorStore(getDatabase, chromaHost); - await store.initialize(); - return store; -} - -/** - * Create a ChromaDB-only vector store (no fallback) - * Use this when you specifically need ChromaDB features - */ -export async function createChromaVectorStore( - host?: string -): Promise { - const store = new ChromaVectorStore(host); - await store.initialize(); - return store; -} - -/** - * Create a SQLite-only vector store - * Use this for embedded/offline scenarios - */ -export async function createSQLiteVectorStore( - getDatabase: () => Database.Database -): Promise { - const store = new SQLiteVectorStore(getDatabase); - await store.initialize(); - return store; -} - -// ============================================================================ -// MIGRATION HELPERS -// ============================================================================ - -/** - * Migrate embeddings from SQLite to ChromaDB - * - * Call this when ChromaDB becomes available to sync any - * embeddings that were stored in SQLite while offline. - */ -export async function migrateToChroma( - sqliteStore: SQLiteVectorStore, - chromaStore: ChromaVectorStore, - onProgress?: (migrated: number, total: number) => void -): Promise<{ migrated: number; failed: number }> { - const sqliteAvailable = await sqliteStore.isAvailable(); - const chromaAvailable = await chromaStore.isAvailable(); - - if (!sqliteAvailable || !chromaAvailable) { - throw new VectorStoreError( - 'Both stores must be available for migration', - 'MIGRATION_PREREQ_FAILED' - ); - } - - // Get all embeddings from SQLite - // This is a simplified implementation - in production you'd want pagination - const stats = await sqliteStore.getStats(); - let migrated = 0; - let failed = 0; - - // Note: This would need access to the underlying database to enumerate all embeddings - // For now, this is a placeholder that shows the pattern - - if (onProgress) { - onProgress(migrated, stats.embeddingCount); - } - - console.log( - `[VectorStore] Migration complete: ${migrated} migrated, ${failed} failed` - ); - - return { migrated, failed }; -} - -// ============================================================================ -// BATCH OPERATIONS -// ============================================================================ - -/** - * Batch upsert embeddings for better performance - */ -export async function batchUpsertEmbeddings( - store: IVectorStore, - items: Array<{ - nodeId: string; - embedding: number[]; - content: string; - metadata?: Record; - }>, - onProgress?: (completed: number, total: number) => void -): Promise<{ succeeded: number; failed: number }> { - let succeeded = 0; - let failed = 0; - - // Process in batches - for (let i = 0; i < items.length; i += BATCH_SIZE) { - const batch = items.slice(i, i + BATCH_SIZE); - - const results = await Promise.allSettled( - batch.map((item) => - store.upsertEmbedding( - item.nodeId, - item.embedding, - item.content, - item.metadata - ) - ) - ); - - for (const result of results) { - if (result.status === 'fulfilled') { - succeeded++; - } else { - failed++; - console.warn('[VectorStore] Batch upsert failed:', result.reason); - } - } - - if (onProgress) { - onProgress(i + batch.length, items.length); - } - } - - return { succeeded, failed }; -} - -/** - * Batch delete embeddings - */ -export async function batchDeleteEmbeddings( - store: IVectorStore, - nodeIds: string[], - onProgress?: (completed: number, total: number) => void -): Promise<{ succeeded: number; failed: number }> { - let succeeded = 0; - let failed = 0; - - // Process in batches - for (let i = 0; i < nodeIds.length; i += BATCH_SIZE) { - const batch = nodeIds.slice(i, i + BATCH_SIZE); - - const results = await Promise.allSettled( - batch.map((nodeId) => store.deleteEmbedding(nodeId)) - ); - - for (const result of results) { - if (result.status === 'fulfilled') { - succeeded++; - } else { - failed++; - } - } - - if (onProgress) { - onProgress(i + batch.length, nodeIds.length); - } - } - - return { succeeded, failed }; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index 7809435..0000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1,1353 +0,0 @@ -#!/usr/bin/env node - -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { z } from 'zod'; -import { VestigeDatabase, VestigeDatabaseError } from './core/database.js'; -import { - captureContext, - formatContextForInjection, - readSavedContext, -} from './core/context-watcher.js'; -import { - IngestInputSchema, - RecallOptionsSchema, - type KnowledgeNode, -} from './core/types.js'; - -// New imports for integrated features -import { FSRSScheduler, Grade, type ReviewGrade } from './core/fsrs.js'; -import { createEmbeddingService, type EmbeddingService } from './core/embeddings.js'; -import { createVectorStore, type IVectorStore } from './core/vector-store.js'; -import { runConsolidation } from './core/consolidation.js'; -import { getConfig, type VestigeConfig } from './core/config.js'; -import { JobQueue } from './jobs/JobQueue.js'; -import { createDecayJobHandler } from './jobs/DecayJob.js'; -import { createREMCycleJobHandler } from './jobs/REMCycleJob.js'; -import { - CacheService, - CACHE_KEYS, - nodeCache, - invalidateNodeCaches, - destroyAllCaches, -} from './services/CacheService.js'; -import { logger, mcpLogger } from './utils/logger.js'; - -// ============================================================================ -// VESTIGE MCP SERVER -// ============================================================================ - -const server = new McpServer({ - name: 'vestige', - version: '0.3.0', -}); - -// Initialize configuration -const config = getConfig(); - -// Initialize database -const db = new VestigeDatabase(); - -// Initialize FSRS scheduler -const fsrsScheduler = new FSRSScheduler({ - desiredRetention: config.fsrs.desiredRetention, - ...(config.fsrs.weights ? { weights: config.fsrs.weights } : {}), -}); - -// Services initialized asynchronously -let embeddingService: EmbeddingService | null = null; -let vectorStore: IVectorStore | null = null; -let jobQueue: JobQueue | null = null; - -// ============================================================================ -// ASYNC SERVICE INITIALIZATION -// ============================================================================ - -async function initializeServices(): Promise { - logger.info('Initializing Vestige services...'); - - // Initialize embedding service (with fallback) - try { - embeddingService = await createEmbeddingService({ - host: config.embeddings.ollamaHost, - model: config.embeddings.model, - }); - logger.info('Embedding service initialized'); - } catch (error) { - logger.warn('Failed to initialize embedding service', { error: String(error) }); - } - - // Initialize vector store - try { - vectorStore = await createVectorStore( - () => (db as unknown as { db: import('better-sqlite3').Database }).db, - config.vectorStore.chromaHost - ); - logger.info('Vector store initialized'); - } catch (error) { - logger.warn('Failed to initialize vector store', { error: String(error) }); - } - - // Initialize job queue - try { - jobQueue = new JobQueue(); - - // Register job handlers - jobQueue.register('decay', createDecayJobHandler(db), { - concurrency: 1, - retryDelay: 60000, // 1 minute - }); - jobQueue.register('rem-cycle', createREMCycleJobHandler(db), { - concurrency: 1, - retryDelay: 300000, // 5 minutes - }); - - // Schedule recurring jobs - if (config.consolidation.enabled) { - // Schedule decay at configured hour (default 3 AM) - jobQueue.schedule('decay', `0 ${config.consolidation.scheduleHour} * * *`, {}); - } - - if (config.rem.enabled) { - // Schedule REM cycle every 6 hours - jobQueue.schedule('rem-cycle', '0 */6 * * *', { - maxAnalyze: config.rem.maxAnalyze, - }); - } - - // Start processing - jobQueue.start(); - logger.info('Job queue initialized and started'); - } catch (error) { - logger.warn('Failed to initialize job queue', { error: String(error) }); - } - - logger.info('Vestige services initialization complete'); -} - -// ============================================================================ -// HELPER: Safe JSON response with error handling -// ============================================================================ - -function safeResponse(data: unknown): { content: Array<{ type: 'text'; text: string }> } { - return { - content: [{ - type: 'text', - text: JSON.stringify(data, null, 2), - }], - }; -} - -function errorResponse(error: unknown): { content: Array<{ type: 'text'; text: string }> } { - const message = error instanceof VestigeDatabaseError - ? { error: error.message, code: error.code } - : { error: error instanceof Error ? error.message : 'Unknown error' }; - - mcpLogger.error('Tool handler error', error instanceof Error ? error : undefined, message); - - return { - content: [{ - type: 'text', - text: JSON.stringify(message, null, 2), - }], - }; -} - -/** - * Wrap a tool handler with error handling - */ -function withErrorHandling(handler: () => Promise): Promise { - return handler().catch(error => { - logger.error('Tool handler error', error instanceof Error ? error : undefined); - throw error; - }); -} - -// ============================================================================ -// RESOURCES -// ============================================================================ - -server.resource( - 'memory://stats', - 'Knowledge base statistics and health status', - async () => { - try { - const health = db.checkHealth(); - const dbSize = db.getDatabaseSize(); - - // Add vector store stats if available - let vectorStats = null; - if (vectorStore) { - try { - vectorStats = await vectorStore.getStats(); - } catch { - // Ignore vector store errors - } - } - - return { - contents: [{ - uri: 'memory://stats', - mimeType: 'application/json', - text: JSON.stringify({ - status: health.status, - totalKnowledgeNodes: health.nodeCount, - totalPeople: health.peopleCount, - totalConnections: health.edgeCount, - databaseSize: dbSize.formatted, - lastBackup: health.lastBackup, - warnings: health.warnings, - vectorStore: vectorStats, - embeddingsAvailable: embeddingService ? await embeddingService.isAvailable() : false, - }, null, 2), - }], - }; - } catch (error) { - return { - contents: [{ - uri: 'memory://stats', - mimeType: 'application/json', - text: JSON.stringify({ error: 'Failed to get stats' }), - }], - }; - } - } -); - -server.resource( - 'memory://knowledge/recent', - 'Recently added knowledge', - async () => { - try { - const result = db.getRecentNodes({ limit: 20 }); - - const formatted = result.items.map(node => ({ - id: node.id, - summary: node.summary || node.content.slice(0, 200), - source: `${node.sourcePlatform}/${node.sourceType}`, - createdAt: node.createdAt.toISOString(), - tags: node.tags, - })); - - return { - contents: [{ - uri: 'memory://knowledge/recent', - mimeType: 'application/json', - text: JSON.stringify({ - total: result.total, - showing: result.items.length, - hasMore: result.hasMore, - items: formatted, - }, null, 2), - }], - }; - } catch (error) { - return { - contents: [{ - uri: 'memory://knowledge/recent', - mimeType: 'application/json', - text: JSON.stringify({ error: 'Failed to get recent knowledge' }), - }], - }; - } - } -); - -server.resource( - 'memory://knowledge/decaying', - 'Knowledge at risk of being forgotten (low retention)', - async () => { - try { - const result = db.getDecayingNodes(0.5, { limit: 20 }); - - const formatted = result.items.map(node => ({ - id: node.id, - summary: node.summary || node.content.slice(0, 200), - retentionStrength: node.retentionStrength, - lastAccessed: node.lastAccessedAt.toISOString(), - daysSinceAccess: Math.floor( - (Date.now() - node.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24) - ), - })); - - return { - contents: [{ - uri: 'memory://knowledge/decaying', - mimeType: 'application/json', - text: JSON.stringify({ - total: result.total, - showing: result.items.length, - hasMore: result.hasMore, - items: formatted, - }, null, 2), - }], - }; - } catch (error) { - return { - contents: [{ - uri: 'memory://knowledge/decaying', - mimeType: 'application/json', - text: JSON.stringify({ error: 'Failed to get decaying knowledge' }), - }], - }; - } - } -); - -server.resource( - 'memory://people/network', - 'Your relationship network', - async () => { - try { - const result = db.getAllPeople({ limit: 50 }); - - const formatted = result.items.map(person => ({ - id: person.id, - name: person.name, - organization: person.organization, - relationshipType: person.relationshipType, - sharedTopics: person.sharedTopics, - lastContact: person.lastContactAt?.toISOString(), - relationshipHealth: person.relationshipHealth, - })); - - return { - contents: [{ - uri: 'memory://people/network', - mimeType: 'application/json', - text: JSON.stringify({ - total: result.total, - showing: result.items.length, - hasMore: result.hasMore, - items: formatted, - }, null, 2), - }], - }; - } catch (error) { - return { - contents: [{ - uri: 'memory://people/network', - mimeType: 'application/json', - text: JSON.stringify({ error: 'Failed to get people network' }), - }], - }; - } - } -); - -server.resource( - 'memory://people/reconnect', - 'People you should reconnect with', - async () => { - try { - const result = db.getPeopleToReconnect(30, { limit: 10 }); - - const formatted = result.items.map(person => { - const daysSince = person.lastContactAt - ? Math.floor((Date.now() - person.lastContactAt.getTime()) / (1000 * 60 * 60 * 24)) - : null; - - return { - id: person.id, - name: person.name, - daysSinceContact: daysSince, - sharedTopics: person.sharedTopics, - howWeMet: person.howWeMet, - suggestion: `Consider reaching out about ${person.sharedTopics[0] || 'catching up'}`, - }; - }); - - return { - contents: [{ - uri: 'memory://people/reconnect', - mimeType: 'application/json', - text: JSON.stringify({ - total: result.total, - showing: result.items.length, - hasMore: result.hasMore, - items: formatted, - }, null, 2), - }], - }; - } catch (error) { - return { - contents: [{ - uri: 'memory://people/reconnect', - mimeType: 'application/json', - text: JSON.stringify({ error: 'Failed to get reconnect suggestions' }), - }], - }; - } - } -); - -server.resource( - 'memory://health', - 'Detailed health status of the memory database', - async () => { - try { - const health = db.checkHealth(); - - return { - contents: [{ - uri: 'memory://health', - mimeType: 'application/json', - text: JSON.stringify(health, null, 2), - }], - }; - } catch (error) { - return { - contents: [{ - uri: 'memory://health', - mimeType: 'application/json', - text: JSON.stringify({ error: 'Failed to get health status' }), - }], - }; - } - } -); - -server.resource( - 'memory://context', - 'Ghost in the Shell - Current system context (active window, clipboard, git)', - async () => { - try { - // Try to read saved context first (from watcher daemon) - let context = readSavedContext(); - - // If no saved context or it's stale (>30 seconds old), capture fresh - if (!context) { - context = captureContext(); - } else { - const age = Date.now() - new Date(context.timestamp).getTime(); - if (age > 30000) { - context = captureContext(); - } - } - - return { - contents: [{ - uri: 'memory://context', - mimeType: 'application/json', - text: JSON.stringify({ - ...context, - injectionString: formatContextForInjection(context), - hint: 'Use injectionString as context prefix when responding to user', - }, null, 2), - }], - }; - } catch (error) { - return { - contents: [{ - uri: 'memory://context', - mimeType: 'application/json', - text: JSON.stringify({ error: 'Failed to capture context' }), - }], - }; - } - } -); - -// ============================================================================ -// TOOLS -// ============================================================================ - -// --- INGESTION --- - -server.tool( - 'ingest', - 'Add new knowledge to the memory palace', - IngestInputSchema.shape, - async (args) => { - try { - const input = IngestInputSchema.parse(args); - - const node = db.insertNode({ - content: input.content, - sourceType: input.source, - sourcePlatform: input.platform, - sourceId: input.sourceId, - sourceUrl: input.sourceUrl, - createdAt: input.timestamp ? new Date(input.timestamp) : new Date(), - updatedAt: new Date(), - lastAccessedAt: new Date(), - accessCount: 0, - retentionStrength: 1.0, - stabilityFactor: 1.0, // New memories start with stability=1 (fast decay) - // sentimentIntensity auto-calculated from content in insertNode - reviewCount: 0, - confidence: 0.8, - isContradicted: false, - contradictionIds: [], - people: input.people || [], - concepts: [], - events: [], - tags: input.tags || [], - sourceChain: [], - }); - - // Generate embedding if service is available - if (embeddingService && vectorStore) { - try { - if (await embeddingService.isAvailable()) { - const embedding = await embeddingService.generateEmbedding(node.content); - await vectorStore.upsertEmbedding(node.id, embedding, node.content, { - sourceType: node.sourceType, - sourcePlatform: node.sourcePlatform, - createdAt: node.createdAt.toISOString(), - }); - mcpLogger.debug('Generated embedding for new node', { nodeId: node.id }); - } - } catch (embeddingError) { - // Log but don't fail the ingest - mcpLogger.warn('Failed to generate embedding', { - nodeId: node.id, - error: String(embeddingError) - }); - } - } - - return safeResponse({ - success: true, - nodeId: node.id, - message: `Knowledge ingested successfully. Node ID: ${node.id}`, - embeddingGenerated: embeddingService ? await embeddingService.isAvailable() : false, - }); - } catch (error) { - return errorResponse(error); - } - } -); - -// --- RETRIEVAL --- - -server.tool( - 'recall', - 'Search and retrieve knowledge from memory', - { - query: z.string().describe('Search query'), - limit: z.number().min(1).max(100).optional().default(10).describe('Maximum results'), - offset: z.number().min(0).optional().default(0).describe('Offset for pagination'), - }, - async (args) => { - try { - const { query, limit, offset } = args as { query: string; limit: number; offset: number }; - - let searchMethod = 'fts'; // Full-text search - let result = db.searchNodes(query, { limit, offset }); - - // Try semantic search first if available and FTS returns few results - if (embeddingService && vectorStore && result.items.length < limit / 2) { - try { - if (await embeddingService.isAvailable()) { - const queryEmbedding = await embeddingService.generateEmbedding(query); - const semanticResults = await vectorStore.findSimilar(queryEmbedding, limit); - - if (semanticResults.length > 0) { - // Get full nodes for semantic results - const semanticNodeIds = new Set(semanticResults.map(r => r.id)); - const existingIds = new Set(result.items.map(n => n.id)); - - // Add semantic results that aren't already in FTS results - for (const semanticResult of semanticResults) { - if (!existingIds.has(semanticResult.id)) { - const node = db.getNode(semanticResult.id); - if (node) { - result.items.push(node); - } - } - } - - searchMethod = 'hybrid'; - } - } - } catch (semanticError) { - mcpLogger.debug('Semantic search fallback failed', { error: String(semanticError) }); - } - } - - // Update access timestamps for retrieved nodes - for (const node of result.items) { - try { - db.updateNodeAccess(node.id); - } catch { - // Ignore access update errors - } - } - - const formatted = result.items.map(node => ({ - id: node.id, - content: node.content, - summary: node.summary, - source: { - type: node.sourceType, - platform: node.sourcePlatform, - url: node.sourceUrl, - }, - metadata: { - createdAt: node.createdAt.toISOString(), - lastAccessed: node.lastAccessedAt.toISOString(), - retentionStrength: node.retentionStrength, - sentimentIntensity: node.sentimentIntensity, // How emotional was this memory? - confidence: node.confidence, - }, - // Git-Blame for Thoughts: What code were you working on when you had this thought? - gitContext: node.gitContext ? { - branch: node.gitContext.branch, - commit: node.gitContext.commit, - message: node.gitContext.commitMessage, - dirty: node.gitContext.dirty, - changedFiles: node.gitContext.changedFiles, - } : undefined, - people: node.people, - tags: node.tags, - })); - - return safeResponse({ - query, - total: result.total, - showing: result.items.length, - offset: result.offset, - hasMore: result.hasMore, - searchMethod, - results: formatted, - }); - } catch (error) { - return errorResponse(error); - } - } -); - -server.tool( - 'get_knowledge', - 'Get a specific knowledge node by ID', - { nodeId: z.string().describe('The ID of the knowledge node to retrieve') }, - async (args) => { - try { - const { nodeId } = args as { nodeId: string }; - - // Try cache first - const cached = nodeCache.get(CACHE_KEYS.node(nodeId)); - if (cached) { - db.updateNodeAccess(nodeId); - return safeResponse(cached); - } - - const node = db.getNode(nodeId); - if (!node) { - return safeResponse({ error: 'Node not found', nodeId }); - } - - db.updateNodeAccess(nodeId); - - // Cache the node - nodeCache.set(CACHE_KEYS.node(nodeId), node); - - return safeResponse(node); - } catch (error) { - return errorResponse(error); - } - } -); - -server.tool( - 'get_related', - 'Find knowledge related to a specific node', - { - nodeId: z.string().describe('The ID of the knowledge node'), - depth: z.number().min(1).max(3).optional().default(1).describe('How many hops to traverse'), - }, - async (args) => { - try { - const { nodeId, depth } = args as { nodeId: string; depth: number }; - - const relatedIds = db.getRelatedNodes(nodeId, depth); - const relatedNodes = relatedIds - .map(id => db.getNode(id)) - .filter((n): n is KnowledgeNode => n !== null); - - return safeResponse({ - sourceNode: nodeId, - depth, - relatedCount: relatedNodes.length, - related: relatedNodes.map(n => ({ - id: n.id, - summary: n.summary || n.content.slice(0, 200), - tags: n.tags, - })), - }); - } catch (error) { - return errorResponse(error); - } - } -); - -// --- SEMANTIC SEARCH --- - -server.tool( - 'semantic_search', - 'Search memories using semantic similarity (requires embeddings)', - { - query: z.string().describe('Search query'), - limit: z.number().min(1).max(50).optional().default(10).describe('Maximum results'), - }, - async (args) => { - try { - const { query, limit } = args as { query: string; limit: number }; - - if (!embeddingService || !await embeddingService.isAvailable()) { - return safeResponse({ - error: 'Embedding service not available', - hint: 'Install Ollama and run: ollama pull nomic-embed-text', - }); - } - - if (!vectorStore) { - return safeResponse({ - error: 'Vector store not available', - }); - } - - const embedding = await embeddingService.generateEmbedding(query); - const similar = await vectorStore.findSimilar(embedding, limit); - - // Get full nodes for results - const results = await Promise.all( - similar.map(async (s) => { - const node = db.getNode(s.id); - if (!node) return null; - - // Update access - try { - db.updateNodeAccess(s.id); - } catch { - // Ignore - } - - return { - id: s.id, - similarity: s.similarity, - content: node.content, - summary: node.summary || node.content.slice(0, 200), - source: { - type: node.sourceType, - platform: node.sourcePlatform, - }, - tags: node.tags, - }; - }) - ); - - return safeResponse({ - query, - method: 'semantic', - results: results.filter(Boolean), - }); - } catch (error) { - return errorResponse(error); - } - } -); - -// --- PEOPLE MEMORY --- - -server.tool( - 'remember_person', - 'Add or update a person in your relationship memory', - { - name: z.string().describe('Person\'s name'), - howWeMet: z.string().optional().describe('How you met this person'), - relationshipType: z.string().optional().describe('Type of relationship (colleague, friend, mentor, etc.)'), - organization: z.string().optional().describe('Their organization/company'), - role: z.string().optional().describe('Their role/title'), - email: z.string().optional().describe('Email address'), - notes: z.string().optional().describe('Any notes about this person'), - sharedTopics: z.array(z.string()).optional().describe('Topics you share interest in'), - }, - async (args) => { - try { - const input = args as { - name: string; - howWeMet?: string; - relationshipType?: string; - organization?: string; - role?: string; - email?: string; - notes?: string; - sharedTopics?: string[]; - }; - - // Check if person exists - const existing = db.getPersonByName(input.name); - if (existing) { - return safeResponse({ - message: `Person "${input.name}" already exists`, - personId: existing.id, - existing: true, - }); - } - - const person = db.insertPerson({ - name: input.name, - aliases: [], - howWeMet: input.howWeMet, - relationshipType: input.relationshipType, - organization: input.organization, - role: input.role, - email: input.email, - notes: input.notes, - sharedTopics: input.sharedTopics || [], - sharedProjects: [], - socialLinks: {}, - contactFrequency: 0, - relationshipHealth: 0.5, - createdAt: new Date(), - updatedAt: new Date(), - }); - - return safeResponse({ - success: true, - personId: person.id, - message: `Remembered ${input.name}`, - }); - } catch (error) { - return errorResponse(error); - } - } -); - -server.tool( - 'get_person', - 'Get information about a person from your memory', - { name: z.string().describe('Person\'s name to look up') }, - async (args) => { - try { - const { name } = args as { name: string }; - - const person = db.getPersonByName(name); - if (!person) { - return safeResponse({ - found: false, - message: `No person named "${name}" found in memory`, - }); - } - - const daysSinceContact = person.lastContactAt - ? Math.floor((Date.now() - person.lastContactAt.getTime()) / (1000 * 60 * 60 * 24)) - : null; - - return safeResponse({ - found: true, - person: { - ...person, - daysSinceContact, - }, - }); - } catch (error) { - return errorResponse(error); - } - } -); - -// --- TEMPORAL / REVIEW --- - -server.tool( - 'mark_reviewed', - 'Mark knowledge as reviewed with FSRS (reinforces memory, slows decay)', - { - nodeId: z.string().describe('The ID of the knowledge node'), - grade: z.number().min(1).max(4).optional().default(3).describe('Review grade: 1=Again, 2=Hard, 3=Good, 4=Easy'), - }, - async (args) => { - try { - const { nodeId, grade } = args as { nodeId: string; grade: number }; - - const nodeBefore = db.getNode(nodeId); - if (!nodeBefore) { - return safeResponse({ error: 'Node not found' }); - } - - // Get current FSRS state or create new one - let currentState = fsrsScheduler.newCard(); - - // If we have previous review data, reconstruct state - if (nodeBefore.reviewCount > 0 && nodeBefore.lastAccessedAt) { - currentState = { - ...currentState, - reps: nodeBefore.reviewCount, - lastReview: nodeBefore.lastAccessedAt, - state: 'Review', - // Estimate stability from retention strength - stability: nodeBefore.stabilityFactor || 1, - }; - } - - // Calculate elapsed days since last review - const elapsedDays = nodeBefore.lastAccessedAt - ? (Date.now() - nodeBefore.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24) - : 0; - - // Apply FSRS review - const reviewResult = fsrsScheduler.review( - currentState, - grade as ReviewGrade, - elapsedDays, - nodeBefore.sentimentIntensity // Apply sentiment boost - ); - - // Update node with FSRS results - db.markReviewed(nodeId); - - // Update stability factor based on FSRS - const internalDb = (db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db; - internalDb.prepare(` - UPDATE knowledge_nodes - SET stability_factor = ?, - next_review_date = ? - WHERE id = ? - `).run( - reviewResult.state.stability, - new Date(Date.now() + reviewResult.interval * 24 * 60 * 60 * 1000).toISOString(), - nodeId - ); - - const nodeAfter = db.getNode(nodeId); - - // Invalidate cache - invalidateNodeCaches(nodeId); - - return safeResponse({ - success: true, - nodeId, - grade: ['Again', 'Hard', 'Good', 'Easy'][grade - 1], - fsrs: { - newStability: reviewResult.state.stability, - newDifficulty: reviewResult.state.difficulty, - retrievability: reviewResult.retrievability, - nextInterval: reviewResult.interval, - isLapse: reviewResult.isLapse, - }, - previousRetention: nodeBefore.retentionStrength, - newRetention: nodeAfter?.retentionStrength, - reviewCount: nodeAfter?.reviewCount, - nextReviewDays: reviewResult.interval, - message: 'Memory reinforced with FSRS', - }); - } catch (error) { - return errorResponse(error); - } - } -); - -// --- CONSOLIDATION --- - -server.tool( - 'run_consolidation', - 'Run sleep consolidation cycle to optimize memories', - {}, - async () => { - try { - const result = await runConsolidation(db, { - shortTermWindowHours: config.consolidation.shortTermWindowHours, - importanceThreshold: config.consolidation.importanceThreshold, - pruneThreshold: config.consolidation.pruneThreshold, - maxAnalyze: config.rem.maxAnalyze, - }); - - return safeResponse({ - success: true, - shortTermProcessed: result.shortTermProcessed, - promoted: result.promotedToLongTerm, - connections: result.connectionsDiscovered, - pruned: result.edgesPruned, - decayed: result.decayApplied, - duration: `${result.duration}ms`, - message: 'Consolidation cycle complete', - }); - } catch (error) { - return errorResponse(error); - } - } -); - -// --- MEMORY STATS --- - -server.tool( - 'get_memory_stats', - 'Get detailed statistics about memory health and distribution', - {}, - async () => { - try { - const stats = db.getStats(); - const health = db.checkHealth(); - - // Get retention strength distribution - type SqliteStatement = { all: () => unknown[] }; - const internalDb = (db as unknown as { db: { prepare: (sql: string) => SqliteStatement } }).db; - - const retentionDist = internalDb.prepare(` - SELECT - CASE - WHEN retention_strength >= 0.8 THEN 'strong' - WHEN retention_strength >= 0.5 THEN 'moderate' - WHEN retention_strength >= 0.3 THEN 'weak' - ELSE 'critical' - END as bucket, - COUNT(*) as count - FROM knowledge_nodes - GROUP BY bucket - ORDER BY - CASE bucket - WHEN 'strong' THEN 1 - WHEN 'moderate' THEN 2 - WHEN 'weak' THEN 3 - ELSE 4 - END - `).all() as Array<{ bucket: string; count: number }>; - - // Get FSRS state distribution - const stabilityDist = internalDb.prepare(` - SELECT - CASE - WHEN stability_factor >= 30 THEN 'stable' - WHEN stability_factor >= 7 THEN 'learning' - WHEN stability_factor >= 1 THEN 'new' - ELSE 'lapsed' - END as bucket, - COUNT(*) as count - FROM knowledge_nodes - GROUP BY bucket - `).all() as Array<{ bucket: string; count: number }>; - - // Get edge statistics - const edgeStatsRows = internalDb.prepare(` - SELECT - COUNT(*) as total, - AVG(weight) as avg_weight, - SUM(CASE WHEN json_extract(metadata, '$.discoveredBy') = 'rem_cycle' THEN 1 ELSE 0 END) as auto_discovered - FROM graph_edges - `).all() as Array<{ total: number; avg_weight: number; auto_discovered: number }>; - const edgeStats = edgeStatsRows[0]; - - // Get vector store stats if available - let vectorStats = null; - if (vectorStore) { - try { - vectorStats = await vectorStore.getStats(); - } catch { - // Ignore - } - } - - // Get job queue stats if available - let jobStats = null; - if (jobQueue) { - jobStats = jobQueue.getStats(); - } - - return safeResponse({ - overview: { - totalNodes: stats.totalNodes, - totalPeople: stats.totalPeople, - totalConnections: stats.totalEdges, - databaseSize: db.getDatabaseSize().formatted, - }, - health: { - status: health.status, - warnings: health.warnings, - }, - retention: { - distribution: retentionDist, - }, - stability: { - distribution: stabilityDist, - }, - connections: { - total: edgeStats?.total || 0, - averageWeight: edgeStats?.avg_weight || 0, - autoDiscovered: edgeStats?.auto_discovered || 0, - }, - vectorStore: vectorStats, - jobQueue: jobStats, - embeddingsAvailable: embeddingService ? await embeddingService.isAvailable() : false, - }); - } catch (error) { - return errorResponse(error); - } - } -); - -// --- DAILY BRIEF --- - -server.tool( - 'daily_brief', - 'Get your daily knowledge brief', - {}, - async () => { - try { - const stats = db.getStats(); - const health = db.checkHealth(); - const decaying = db.getDecayingNodes(0.5, { limit: 5 }); - const reconnect = db.getPeopleToReconnect(30, { limit: 5 }); - const recent = db.getRecentNodes({ limit: 5 }); - - const brief = { - date: new Date().toISOString().split('T')[0], - greeting: getTimeBasedGreeting(), - healthStatus: health.status, - warnings: health.warnings.length > 0 ? health.warnings : undefined, - stats: { - totalKnowledge: stats.totalNodes, - peopleInNetwork: stats.totalPeople, - connections: stats.totalEdges, - databaseSize: db.getDatabaseSize().formatted, - }, - reviewNeeded: decaying.items.map(n => ({ - id: n.id, - preview: n.summary || n.content.slice(0, 100), - retentionStrength: n.retentionStrength, - daysSinceAccess: Math.floor( - (Date.now() - n.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24) - ), - })), - peopleToReconnect: reconnect.items.map(p => ({ - name: p.name, - daysSinceContact: p.lastContactAt - ? Math.floor((Date.now() - p.lastContactAt.getTime()) / (1000 * 60 * 60 * 24)) - : null, - sharedTopics: p.sharedTopics, - })), - recentlyAdded: recent.items.map(n => ({ - id: n.id, - preview: n.summary || n.content.slice(0, 100), - source: n.sourcePlatform, - })), - }; - - return safeResponse(brief); - } catch (error) { - return errorResponse(error); - } - } -); - -// --- HEALTH & MAINTENANCE --- - -server.tool( - 'health_check', - 'Get detailed health status of the memory database', - {}, - async () => { - try { - const health = db.checkHealth(); - const size = db.getDatabaseSize(); - - return safeResponse({ - ...health, - databaseSize: size, - recommendations: getHealthRecommendations(health), - }); - } catch (error) { - return errorResponse(error); - } - } -); - -server.tool( - 'backup', - 'Create a backup of the memory database', - {}, - async () => { - try { - const backupPath = db.backup(); - const backups = db.listBackups(); - - return safeResponse({ - success: true, - backupPath, - message: 'Backup created successfully', - totalBackups: backups.length, - backups: backups.slice(0, 5).map(b => ({ - path: b.path, - size: `${(b.size / 1024 / 1024).toFixed(2)}MB`, - date: b.date.toISOString(), - })), - }); - } catch (error) { - return errorResponse(error); - } - } -); - -server.tool( - 'list_backups', - 'List available database backups', - {}, - async () => { - try { - const backups = db.listBackups(); - - return safeResponse({ - totalBackups: backups.length, - backups: backups.map(b => ({ - path: b.path, - size: `${(b.size / 1024 / 1024).toFixed(2)}MB`, - date: b.date.toISOString(), - })), - }); - } catch (error) { - return errorResponse(error); - } - } -); - -server.tool( - 'optimize_database', - 'Optimize the database (vacuum, reindex) - use sparingly', - {}, - async () => { - try { - const sizeBefore = db.getDatabaseSize(); - db.optimize(); - const sizeAfter = db.getDatabaseSize(); - - return safeResponse({ - success: true, - message: 'Database optimized', - sizeBefore: sizeBefore.formatted, - sizeAfter: sizeAfter.formatted, - spaceSaved: `${(sizeBefore.mb - sizeAfter.mb).toFixed(2)}MB`, - }); - } catch (error) { - return errorResponse(error); - } - } -); - -server.tool( - 'apply_decay', - 'Apply memory decay based on time since last access', - {}, - async () => { - try { - const updatedCount = db.applyDecay(); - - return safeResponse({ - success: true, - nodesUpdated: updatedCount, - message: `Applied decay to ${updatedCount} knowledge nodes`, - }); - } catch (error) { - return errorResponse(error); - } - } -); - -// ============================================================================ -// HELPERS -// ============================================================================ - -function getTimeBasedGreeting(): string { - const hour = new Date().getHours(); - if (hour < 12) return 'Good morning'; - if (hour < 17) return 'Good afternoon'; - return 'Good evening'; -} - -function getHealthRecommendations(health: ReturnType): string[] { - const recommendations: string[] = []; - - if (health.status === 'critical') { - recommendations.push('CRITICAL: Immediate attention required. Check warnings for details.'); - } - - if (!health.lastBackup) { - recommendations.push('Create your first backup using the backup tool'); - } else { - const daysSinceBackup = (Date.now() - new Date(health.lastBackup).getTime()) / (1000 * 60 * 60 * 24); - if (daysSinceBackup > 7) { - recommendations.push(`Consider creating a backup (last backup was ${Math.floor(daysSinceBackup)} days ago)`); - } - } - - if (health.dbSizeMB > 50) { - recommendations.push('Consider running optimize_database to reclaim space'); - } - - if (health.nodeCount > 10000) { - recommendations.push('Large knowledge base detected. Searches may be slower.'); - } - - if (recommendations.length === 0) { - recommendations.push('Everything looks healthy!'); - } - - return recommendations; -} - -// ============================================================================ -// GRACEFUL SHUTDOWN -// ============================================================================ - -async function gracefulShutdown(): Promise { - logger.info('Shutting down Vestige...'); - - // Stop job queue - if (jobQueue) { - try { - await jobQueue.shutdown(10000); // 10 second timeout - logger.info('Job queue stopped'); - } catch (error) { - logger.warn('Error stopping job queue', { error: String(error) }); - } - } - - // Close vector store - if (vectorStore) { - try { - await vectorStore.close(); - logger.info('Vector store closed'); - } catch (error) { - logger.warn('Error closing vector store', { error: String(error) }); - } - } - - // Destroy all caches - destroyAllCaches(); - logger.info('Caches destroyed'); - - // Close database - db.close(); - logger.info('Database closed'); - - logger.info('Vestige shutdown complete'); -} - -process.on('SIGINT', async () => { - await gracefulShutdown(); - process.exit(0); -}); - -process.on('SIGTERM', async () => { - await gracefulShutdown(); - process.exit(0); -}); - -// ============================================================================ -// START SERVER -// ============================================================================ - -async function main() { - // Initialize async services - await initializeServices(); - - const transport = new StdioServerTransport(); - await server.connect(transport); - logger.info('Vestige MCP server v0.3.0 running'); -} - -main().catch((error) => { - logger.error('Failed to start Vestige', error instanceof Error ? error : undefined); - db.close(); - process.exit(1); -}); diff --git a/packages/core/src/jobs/ConsolidationJob.ts b/packages/core/src/jobs/ConsolidationJob.ts deleted file mode 100644 index 599d926..0000000 --- a/packages/core/src/jobs/ConsolidationJob.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * ConsolidationJob - Knowledge Consolidation Processing - * - * Consolidates related knowledge nodes by: - * - Merging highly similar nodes - * - Strengthening frequently co-accessed node clusters - * - Pruning orphaned edges - * - Optimizing the database - * - * Designed to run as a scheduled background job (e.g., weekly). - * - * @module jobs/ConsolidationJob - */ - -import type { VestigeDatabase } from '../core/database.js'; -import type { Job, JobHandler } from './JobQueue.js'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface ConsolidationJobData { - /** Minimum similarity threshold for merging nodes (0-1). Default: 0.95 */ - mergeThreshold?: number; - /** Whether to prune orphaned edges. Default: true */ - pruneOrphanedEdges?: boolean; - /** Whether to optimize database after consolidation. Default: true */ - optimizeDb?: boolean; - /** Whether to run in dry-run mode (analysis only). Default: false */ - dryRun?: boolean; -} - -export interface ConsolidationJobResult { - /** Number of node pairs analyzed for similarity */ - pairsAnalyzed: number; - /** Number of nodes merged (dry run: would be merged) */ - nodesMerged: number; - /** Number of orphaned edges pruned */ - edgesPruned: number; - /** Number of edge weights updated (strengthened) */ - edgesStrengthened: number; - /** Whether database optimization was performed */ - databaseOptimized: boolean; - /** Time taken in milliseconds */ - duration: number; - /** Timestamp when the job ran */ - timestamp: Date; -} - -// ============================================================================ -// CONSOLIDATION LOGIC -// ============================================================================ - -/** - * Run knowledge consolidation on the database - */ -async function runConsolidation( - db: VestigeDatabase, - options: { - mergeThreshold?: number; - pruneOrphanedEdges?: boolean; - optimizeDb?: boolean; - dryRun?: boolean; - } = {} -): Promise { - const startTime = Date.now(); - const { - mergeThreshold = 0.95, - pruneOrphanedEdges = true, - optimizeDb = true, - dryRun = false, - } = options; - - const result: ConsolidationJobResult = { - pairsAnalyzed: 0, - nodesMerged: 0, - edgesPruned: 0, - edgesStrengthened: 0, - databaseOptimized: false, - duration: 0, - timestamp: new Date(), - }; - - // Step 1: Analyze and strengthen co-accessed clusters - // (Nodes accessed together frequently should have stronger edges) - const stats = db.getStats(); - result.pairsAnalyzed = Math.min(stats.totalNodes * (stats.totalNodes - 1) / 2, 10000); - - // Step 2: Prune orphaned edges (edges pointing to deleted nodes) - // In a real implementation, this would query for edges with invalid node references - if (pruneOrphanedEdges && !dryRun) { - // The database foreign keys should handle this, but we can do a sanity check - // For now, we just report 0 pruned as SQLite handles this via ON DELETE CASCADE - result.edgesPruned = 0; - } - - // Step 3: Optimize database - if (optimizeDb && !dryRun) { - try { - db.optimize(); - result.databaseOptimized = true; - } catch { - // Log but don't fail the job - result.databaseOptimized = false; - } - } - - result.duration = Date.now() - startTime; - return result; -} - -// ============================================================================ -// JOB HANDLER FACTORY -// ============================================================================ - -/** - * Create a consolidation job handler - * - * @param db - VestigeDatabase instance - * @returns Job handler function - * - * @example - * ```typescript - * const db = new VestigeDatabase(); - * const queue = new JobQueue(); - * - * queue.register('consolidation', createConsolidationJobHandler(db), { - * concurrency: 1, // Only one consolidation at a time - * retryDelay: 3600000, // Wait 1 hour before retry - * }); - * - * // Schedule to run weekly on Sunday at 4 AM - * queue.schedule('consolidation', '0 4 * * 0', {}); - * ``` - */ -export function createConsolidationJobHandler( - db: VestigeDatabase -): JobHandler { - return async (job: Job): Promise => { - return runConsolidation(db, { - mergeThreshold: job.data.mergeThreshold, - pruneOrphanedEdges: job.data.pruneOrphanedEdges, - optimizeDb: job.data.optimizeDb, - dryRun: job.data.dryRun, - }); - }; -} - -// ============================================================================ -// HELPER FUNCTIONS -// ============================================================================ - -/** - * Preview what consolidation would do without making changes - */ -export async function previewConsolidation( - db: VestigeDatabase -): Promise { - return runConsolidation(db, { dryRun: true }); -} - -/** - * Get database health metrics relevant to consolidation - */ -export function getConsolidationMetrics(db: VestigeDatabase): { - totalNodes: number; - totalEdges: number; - databaseSizeMB: number; - needsOptimization: boolean; -} { - const stats = db.getStats(); - const size = db.getDatabaseSize(); - const health = db.checkHealth(); - - return { - totalNodes: stats.totalNodes, - totalEdges: stats.totalEdges, - databaseSizeMB: size.mb, - needsOptimization: health.status !== 'healthy' || size.mb > 50, - }; -} diff --git a/packages/core/src/jobs/DecayJob.ts b/packages/core/src/jobs/DecayJob.ts deleted file mode 100644 index 55042ae..0000000 --- a/packages/core/src/jobs/DecayJob.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * DecayJob - Memory Decay Processing - * - * Applies the Ebbinghaus forgetting curve to all knowledge nodes, - * updating their retention strength based on time since last access. - * - * Designed to run as a scheduled background job (e.g., daily at 3 AM). - * - * @module jobs/DecayJob - */ - -import type { VestigeDatabase } from '../core/database.js'; -import type { Job, JobHandler } from './JobQueue.js'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface DecayJobData { - /** Optional: Minimum retention threshold to skip already-decayed nodes */ - minRetention?: number; - /** Optional: Maximum number of nodes to process in one batch */ - batchSize?: number; -} - -export interface DecayJobResult { - /** Number of nodes whose retention was updated */ - updatedCount: number; - /** Total time taken in milliseconds */ - processingTime: number; - /** Timestamp when the job ran */ - timestamp: Date; -} - -// ============================================================================ -// JOB HANDLER FACTORY -// ============================================================================ - -/** - * Create a decay job handler - * - * @param db - VestigeDatabase instance - * @returns Job handler function - * - * @example - * ```typescript - * const db = new VestigeDatabase(); - * const queue = new JobQueue(); - * - * queue.register('decay', createDecayJobHandler(db), { - * concurrency: 1, // Only one decay job at a time - * retryDelay: 60000, // Wait 1 minute before retry - * }); - * - * // Schedule to run daily at 3 AM - * queue.schedule('decay', '0 3 * * *', {}); - * ``` - */ -export function createDecayJobHandler( - db: VestigeDatabase -): JobHandler { - return async (job: Job): Promise => { - const startTime = Date.now(); - - // Apply decay to all nodes - // The database method handles the Ebbinghaus curve calculation - const updatedCount = db.applyDecay(); - - const result: DecayJobResult = { - updatedCount, - processingTime: Date.now() - startTime, - timestamp: new Date(), - }; - - return result; - }; -} - -// ============================================================================ -// HELPER FUNCTIONS -// ============================================================================ - -/** - * Get nodes that are critically decayed (retention < threshold) - * Useful for generating review notifications - */ -export async function getCriticallyDecayedNodes( - db: VestigeDatabase, - threshold: number = 0.3 -): Promise<{ nodeId: string; retention: number; content: string }[]> { - const result = db.getDecayingNodes(threshold, { limit: 50 }); - - return result.items.map(node => ({ - nodeId: node.id, - retention: node.retentionStrength, - content: node.content.slice(0, 100), - })); -} diff --git a/packages/core/src/jobs/JobQueue.ts b/packages/core/src/jobs/JobQueue.ts deleted file mode 100644 index 3c88d16..0000000 --- a/packages/core/src/jobs/JobQueue.ts +++ /dev/null @@ -1,809 +0,0 @@ -/** - * JobQueue - Background Job Processing for Vestige MCP - * - * A production-ready in-memory job queue with: - * - Priority-based job scheduling - * - Retry logic with exponential backoff - * - Concurrency control per job type - * - Event-driven architecture - * - Cron-like scheduling support - * - * @module jobs/JobQueue - */ - -import { EventEmitter } from 'events'; -import { nanoid } from 'nanoid'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export type JobStatus = 'pending' | 'running' | 'completed' | 'failed'; - -export interface Job { - id: string; - name: string; - data: T; - priority: number; - createdAt: Date; - scheduledAt?: Date; - startedAt?: Date; - completedAt?: Date; - retryCount: number; - maxRetries: number; - status: JobStatus; - error?: string; -} - -export interface JobResult { - jobId: string; - success: boolean; - result?: R; - error?: Error; - duration: number; -} - -export type JobHandler = (job: Job) => Promise; - -export interface JobOptions { - /** Priority (higher = processed first). Default: 0 */ - priority?: number; - /** Delay in milliseconds before job becomes eligible. Default: 0 */ - delay?: number; - /** Maximum retry attempts on failure. Default: 3 */ - maxRetries?: number; -} - -export interface JobDefinition { - name: string; - handler: JobHandler; - concurrency: number; - retryDelay: number; -} - -export interface ScheduledJob { - name: string; - cronExpression: string; - data: unknown; - lastRun?: Date; - nextRun?: Date; -} - -export interface QueueStats { - pending: number; - running: number; - completed: number; - failed: number; - total: number; -} - -// ============================================================================ -// JOB QUEUE EVENTS -// ============================================================================ - -export interface JobQueueEvents { - 'job:added': (job: Job) => void; - 'job:started': (job: Job) => void; - 'job:completed': (job: Job, result: JobResult) => void; - 'job:failed': (job: Job, error: Error) => void; - 'job:retry': (job: Job, attempt: number, error: Error) => void; - 'queue:drained': () => void; - 'queue:error': (error: Error) => void; -} - -// ============================================================================ -// CRON PARSER (Simple Implementation) -// ============================================================================ - -interface CronFields { - minute: number[]; - hour: number[]; - dayOfMonth: number[]; - month: number[]; - dayOfWeek: number[]; -} - -/** - * Parse a simple cron expression - * Format: minute hour day-of-month month day-of-week - * Supports: numbers, *, /step, ranges (-) - */ -function parseCronField(field: string, min: number, max: number): number[] { - const values: number[] = []; - - // Handle wildcard - if (field === '*') { - for (let i = min; i <= max; i++) { - values.push(i); - } - return values; - } - - // Handle step values (*/n or n/m) - if (field.includes('/')) { - const [range, stepStr] = field.split('/'); - const step = parseInt(stepStr || '1', 10); - let start = min; - let end = max; - - if (range && range !== '*') { - if (range.includes('-')) { - const [s, e] = range.split('-'); - start = parseInt(s || String(min), 10); - end = parseInt(e || String(max), 10); - } else { - start = parseInt(range, 10); - } - } - - for (let i = start; i <= end; i += step) { - values.push(i); - } - return values; - } - - // Handle ranges (n-m) - if (field.includes('-')) { - const [start, end] = field.split('-'); - const s = parseInt(start || String(min), 10); - const e = parseInt(end || String(max), 10); - for (let i = s; i <= e; i++) { - values.push(i); - } - return values; - } - - // Handle comma-separated values - if (field.includes(',')) { - return field.split(',').map(v => parseInt(v.trim(), 10)); - } - - // Single value - values.push(parseInt(field, 10)); - return values; -} - -function parseCronExpression(expression: string): CronFields { - const parts = expression.trim().split(/\s+/); - - if (parts.length !== 5) { - throw new Error(`Invalid cron expression: ${expression}. Expected 5 fields.`); - } - - return { - minute: parseCronField(parts[0] || '*', 0, 59), - hour: parseCronField(parts[1] || '*', 0, 23), - dayOfMonth: parseCronField(parts[2] || '*', 1, 31), - month: parseCronField(parts[3] || '*', 1, 12), - dayOfWeek: parseCronField(parts[4] || '*', 0, 6), - }; -} - -function getNextCronDate(expression: string, after: Date = new Date()): Date { - const fields = parseCronExpression(expression); - const next = new Date(after); - next.setSeconds(0); - next.setMilliseconds(0); - - // Start from next minute - next.setMinutes(next.getMinutes() + 1); - - // Find next matching time (limit iterations to prevent infinite loops) - for (let iterations = 0; iterations < 525600; iterations++) { // Max 1 year of minutes - const minute = next.getMinutes(); - const hour = next.getHours(); - const dayOfMonth = next.getDate(); - const month = next.getMonth() + 1; // JS months are 0-indexed - const dayOfWeek = next.getDay(); - - // Check if current time matches cron expression - if ( - fields.minute.includes(minute) && - fields.hour.includes(hour) && - fields.dayOfMonth.includes(dayOfMonth) && - fields.month.includes(month) && - fields.dayOfWeek.includes(dayOfWeek) - ) { - return next; - } - - // Advance by one minute - next.setMinutes(next.getMinutes() + 1); - } - - throw new Error(`Could not find next cron date within 1 year for: ${expression}`); -} - -// ============================================================================ -// JOB QUEUE IMPLEMENTATION -// ============================================================================ - -export class JobQueue extends EventEmitter { - private jobs: Map = new Map(); - private handlers: Map = new Map(); - private running: Map = new Map(); - private interval: NodeJS.Timeout | null = null; - private scheduledJobs: Map = new Map(); - private schedulerInterval: NodeJS.Timeout | null = null; - private isProcessing = false; - private isPaused = false; - - // Completed/failed job history (limited size) - private readonly maxHistorySize = 1000; - private completedJobIds: Set = new Set(); - private failedJobIds: Set = new Set(); - - constructor() { - super(); - this.setMaxListeners(100); - } - - // ============================================================================ - // HANDLER REGISTRATION - // ============================================================================ - - /** - * Register a job handler - * - * @param name - Unique job type name - * @param handler - Async function to process the job - * @param options - Handler options (concurrency, retryDelay) - * - * @example - * ```typescript - * queue.register('send-email', async (job) => { - * await sendEmail(job.data); - * return { sent: true }; - * }, { concurrency: 5, retryDelay: 5000 }); - * ``` - */ - register( - name: string, - handler: JobHandler, - options?: { concurrency?: number; retryDelay?: number } - ): void { - if (this.handlers.has(name)) { - throw new Error(`Handler already registered for job type: ${name}`); - } - - // Store as JobDefinition since we type-erase at runtime - // The type safety is maintained at the call site (add/register) - const definition: JobDefinition = { - name, - handler: handler as unknown as JobHandler, - concurrency: options?.concurrency ?? 1, - retryDelay: options?.retryDelay ?? 1000, - }; - - this.handlers.set(name, definition); - this.running.set(name, 0); - } - - /** - * Unregister a job handler - */ - unregister(name: string): boolean { - const deleted = this.handlers.delete(name); - this.running.delete(name); - return deleted; - } - - // ============================================================================ - // JOB MANAGEMENT - // ============================================================================ - - /** - * Add a job to the queue - * - * @param name - Job type name (must have registered handler) - * @param data - Job data payload - * @param options - Job options (priority, delay, maxRetries) - * @returns Job ID - * - * @example - * ```typescript - * const jobId = queue.add('send-email', { - * to: 'user@example.com', - * subject: 'Hello' - * }, { priority: 10, maxRetries: 5 }); - * ``` - */ - add( - name: string, - data: T, - options?: JobOptions - ): string { - if (!this.handlers.has(name)) { - throw new Error(`No handler registered for job type: ${name}`); - } - - const id = nanoid(); - const now = new Date(); - - let scheduledAt: Date | undefined; - if (options?.delay && options.delay > 0) { - scheduledAt = new Date(now.getTime() + options.delay); - } - - const job: Job = { - id, - name, - data, - priority: options?.priority ?? 0, - createdAt: now, - scheduledAt, - retryCount: 0, - maxRetries: options?.maxRetries ?? 3, - status: 'pending', - }; - - this.jobs.set(id, job as Job); - this.emit('job:added', job); - - // Trigger processing if running - if (this.isProcessing && !this.isPaused) { - this.processNextJobs(); - } - - return id; - } - - /** - * Get a job by ID - */ - getJob(id: string): Job | undefined { - return this.jobs.get(id); - } - - /** - * Get all jobs matching a filter - */ - getJobs(filter?: { name?: string; status?: JobStatus }): Job[] { - let jobs = Array.from(this.jobs.values()); - - if (filter?.name) { - jobs = jobs.filter(j => j.name === filter.name); - } - - if (filter?.status) { - jobs = jobs.filter(j => j.status === filter.status); - } - - return jobs; - } - - /** - * Remove a job from the queue - * Can only remove pending jobs - */ - removeJob(id: string): boolean { - const job = this.jobs.get(id); - if (!job) return false; - - if (job.status === 'running') { - throw new Error('Cannot remove a running job'); - } - - return this.jobs.delete(id); - } - - /** - * Clear all completed/failed jobs from history - */ - clearHistory(): void { - for (const id of this.completedJobIds) { - this.jobs.delete(id); - } - for (const id of this.failedJobIds) { - this.jobs.delete(id); - } - this.completedJobIds.clear(); - this.failedJobIds.clear(); - } - - // ============================================================================ - // QUEUE STATISTICS - // ============================================================================ - - /** - * Get queue statistics - */ - getStats(): QueueStats { - let pending = 0; - let running = 0; - let completed = 0; - let failed = 0; - - for (const job of this.jobs.values()) { - switch (job.status) { - case 'pending': - pending++; - break; - case 'running': - running++; - break; - case 'completed': - completed++; - break; - case 'failed': - failed++; - break; - } - } - - return { - pending, - running, - completed, - failed, - total: this.jobs.size, - }; - } - - /** - * Check if queue is empty (no pending or running jobs) - */ - isEmpty(): boolean { - for (const job of this.jobs.values()) { - if (job.status === 'pending' || job.status === 'running') { - return false; - } - } - return true; - } - - // ============================================================================ - // PROCESSING - // ============================================================================ - - /** - * Start processing jobs - * - * @param pollInterval - How often to check for new jobs (ms). Default: 100 - */ - start(pollInterval: number = 100): void { - if (this.isProcessing) { - return; - } - - this.isProcessing = true; - this.isPaused = false; - - this.interval = setInterval(() => { - if (!this.isPaused) { - this.processNextJobs(); - } - }, pollInterval); - - // Start scheduler for cron jobs - this.startScheduler(); - - // Process immediately - this.processNextJobs(); - } - - /** - * Stop processing jobs - */ - stop(): void { - this.isProcessing = false; - - if (this.interval) { - clearInterval(this.interval); - this.interval = null; - } - - this.stopScheduler(); - } - - /** - * Pause processing (jobs stay in queue) - */ - pause(): void { - this.isPaused = true; - } - - /** - * Resume processing - */ - resume(): void { - this.isPaused = false; - this.processNextJobs(); - } - - /** - * Wait for all pending jobs to complete - */ - async drain(): Promise { - return new Promise((resolve) => { - const check = () => { - if (this.isEmpty()) { - resolve(); - } else { - setTimeout(check, 50); - } - }; - check(); - }); - } - - /** - * Process next eligible jobs - */ - private processNextJobs(): void { - const now = new Date(); - - // Get pending jobs sorted by priority (descending) - const pendingJobs = Array.from(this.jobs.values()) - .filter(job => { - if (job.status !== 'pending') return false; - if (job.scheduledAt && job.scheduledAt > now) return false; - return true; - }) - .sort((a, b) => b.priority - a.priority); - - // Process jobs respecting concurrency limits - for (const job of pendingJobs) { - const definition = this.handlers.get(job.name); - if (!definition) continue; - - const currentRunning = this.running.get(job.name) ?? 0; - if (currentRunning >= definition.concurrency) continue; - - // Start processing this job - this.processJob(job, definition); - } - } - - /** - * Process a single job - */ - private async processJob(job: Job, definition: JobDefinition): Promise { - // Update job status - job.status = 'running'; - job.startedAt = new Date(); - - // Track running count - const currentRunning = this.running.get(job.name) ?? 0; - this.running.set(job.name, currentRunning + 1); - - this.emit('job:started', job); - - const startTime = Date.now(); - - try { - const result = await definition.handler(job); - - // Job completed successfully - job.status = 'completed'; - job.completedAt = new Date(); - - const jobResult: JobResult = { - jobId: job.id, - success: true, - result, - duration: Date.now() - startTime, - }; - - this.emit('job:completed', job, jobResult); - - // Track in history - this.addToHistory(job.id, 'completed'); - - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - - // Check if we should retry - if (job.retryCount < job.maxRetries) { - job.retryCount++; - job.status = 'pending'; - - // Schedule retry with exponential backoff - const backoffDelay = definition.retryDelay * Math.pow(2, job.retryCount - 1); - job.scheduledAt = new Date(Date.now() + backoffDelay); - - this.emit('job:retry', job, job.retryCount, err); - - } else { - // Max retries exceeded - mark as failed - job.status = 'failed'; - job.completedAt = new Date(); - job.error = err.message; - - this.emit('job:failed', job, err); - - // Track in history - this.addToHistory(job.id, 'failed'); - } - - } finally { - // Update running count - const runningCount = this.running.get(job.name) ?? 1; - this.running.set(job.name, Math.max(0, runningCount - 1)); - - // Check if queue is drained - if (this.isEmpty()) { - this.emit('queue:drained'); - } - } - } - - /** - * Add job to history tracking (with size limit) - */ - private addToHistory(jobId: string, type: 'completed' | 'failed'): void { - const targetSet = type === 'completed' ? this.completedJobIds : this.failedJobIds; - targetSet.add(jobId); - - // Trim history if too large - if (targetSet.size > this.maxHistorySize) { - const iterator = targetSet.values(); - const firstValue = iterator.next().value; - if (firstValue) { - targetSet.delete(firstValue); - this.jobs.delete(firstValue); - } - } - } - - // ============================================================================ - // SCHEDULING (CRON-LIKE) - // ============================================================================ - - /** - * Schedule a recurring job - * - * @param name - Job type name - * @param cronExpression - Cron expression (minute hour day-of-month month day-of-week) - * @param data - Job data payload - * - * @example - * ```typescript - * // Run decay at 3 AM daily - * queue.schedule('decay', '0 3 * * *', {}); - * - * // Run REM cycle every 6 hours - * queue.schedule('rem-cycle', '0 *\\/6 * * *', {}); - * ``` - */ - schedule(name: string, cronExpression: string, data: T): void { - if (!this.handlers.has(name)) { - throw new Error(`No handler registered for job type: ${name}`); - } - - // Validate cron expression by parsing it - try { - parseCronExpression(cronExpression); - } catch (error) { - throw new Error(`Invalid cron expression for ${name}: ${cronExpression}`); - } - - const scheduledJob: ScheduledJob = { - name, - cronExpression, - data, - nextRun: getNextCronDate(cronExpression), - }; - - this.scheduledJobs.set(name, scheduledJob); - } - - /** - * Remove a scheduled job - */ - unschedule(name: string): boolean { - return this.scheduledJobs.delete(name); - } - - /** - * Get all scheduled jobs - */ - getScheduledJobs(): ScheduledJob[] { - return Array.from(this.scheduledJobs.values()); - } - - /** - * Start the scheduler - */ - private startScheduler(): void { - if (this.schedulerInterval) return; - - // Check every minute for scheduled jobs - this.schedulerInterval = setInterval(() => { - this.checkScheduledJobs(); - }, 60000); - - // Also check immediately - this.checkScheduledJobs(); - } - - /** - * Stop the scheduler - */ - private stopScheduler(): void { - if (this.schedulerInterval) { - clearInterval(this.schedulerInterval); - this.schedulerInterval = null; - } - } - - /** - * Check and trigger scheduled jobs - */ - private checkScheduledJobs(): void { - const now = new Date(); - - for (const [name, scheduled] of this.scheduledJobs) { - if (scheduled.nextRun && scheduled.nextRun <= now) { - try { - // Add the job - this.add(name, scheduled.data); - - // Update last run and calculate next run - scheduled.lastRun = now; - scheduled.nextRun = getNextCronDate(scheduled.cronExpression, now); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - this.emit('queue:error', err); - } - } - } - } - - // ============================================================================ - // CLEANUP - // ============================================================================ - - /** - * Graceful shutdown - */ - async shutdown(timeout: number = 30000): Promise { - this.stop(); - this.isPaused = true; - - // Wait for running jobs to complete (with timeout) - const waitStart = Date.now(); - - while (Date.now() - waitStart < timeout) { - const stats = this.getStats(); - if (stats.running === 0) { - break; - } - await new Promise(resolve => setTimeout(resolve, 100)); - } - - // Clear all jobs - this.jobs.clear(); - this.completedJobIds.clear(); - this.failedJobIds.clear(); - this.scheduledJobs.clear(); - - this.removeAllListeners(); - } -} - -// ============================================================================ -// SINGLETON INSTANCE (Optional) -// ============================================================================ - -let defaultQueue: JobQueue | null = null; - -/** - * Get the default job queue instance - */ -export function getDefaultQueue(): JobQueue { - if (!defaultQueue) { - defaultQueue = new JobQueue(); - } - return defaultQueue; -} - -/** - * Reset the default queue (for testing) - */ -export function resetDefaultQueue(): void { - if (defaultQueue) { - defaultQueue.shutdown().catch(() => {}); - defaultQueue = null; - } -} diff --git a/packages/core/src/jobs/REMCycleJob.ts b/packages/core/src/jobs/REMCycleJob.ts deleted file mode 100644 index 203d41b..0000000 --- a/packages/core/src/jobs/REMCycleJob.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * REMCycleJob - Connection Discovery Processing - * - * Runs the REM (Rapid Eye Movement) cycle to discover hidden connections - * between knowledge nodes using semantic similarity, shared concepts, - * and keyword overlap analysis. - * - * Designed to run as a scheduled background job (e.g., every 6 hours). - * - * @module jobs/REMCycleJob - */ - -import type { VestigeDatabase } from '../core/database.js'; -import { runREMCycle } from '../core/rem-cycle.js'; -import type { Job, JobHandler } from './JobQueue.js'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface REMCycleJobData { - /** Maximum number of nodes to analyze per cycle. Default: 50 */ - maxAnalyze?: number; - /** Minimum connection strength threshold (0-1). Default: 0.3 */ - minStrength?: number; - /** If true, only discover but don't create edges. Default: false */ - dryRun?: boolean; -} - -export interface REMCycleJobResult { - /** Number of nodes analyzed */ - nodesAnalyzed: number; - /** Number of potential connections discovered */ - connectionsDiscovered: number; - /** Number of graph edges actually created */ - connectionsCreated: number; - /** Time taken in milliseconds */ - duration: number; - /** Details of discovered connections */ - discoveries: Array<{ - nodeA: string; - nodeB: string; - reason: string; - }>; - /** Timestamp when the job ran */ - timestamp: Date; -} - -// ============================================================================ -// JOB HANDLER FACTORY -// ============================================================================ - -/** - * Create a REM cycle job handler - * - * @param db - VestigeDatabase instance - * @returns Job handler function - * - * @example - * ```typescript - * const db = new VestigeDatabase(); - * const queue = new JobQueue(); - * - * queue.register('rem-cycle', createREMCycleJobHandler(db), { - * concurrency: 1, // Only one REM cycle at a time - * retryDelay: 300000, // Wait 5 minutes before retry - * }); - * - * // Schedule to run every 6 hours - * queue.schedule('rem-cycle', '0 *\/6 * * *', { maxAnalyze: 100 }); - * ``` - */ -export function createREMCycleJobHandler( - db: VestigeDatabase -): JobHandler { - return async (job: Job): Promise => { - const options = { - maxAnalyze: job.data.maxAnalyze ?? 50, - minStrength: job.data.minStrength ?? 0.3, - dryRun: job.data.dryRun ?? false, - }; - - // Run the REM cycle (async) - const cycleResult = await runREMCycle(db, options); - - const result: REMCycleJobResult = { - nodesAnalyzed: cycleResult.nodesAnalyzed, - connectionsDiscovered: cycleResult.connectionsDiscovered, - connectionsCreated: cycleResult.connectionsCreated, - duration: cycleResult.duration, - discoveries: cycleResult.discoveries.map(d => ({ - nodeA: d.nodeA, - nodeB: d.nodeB, - reason: d.reason, - })), - timestamp: new Date(), - }; - - return result; - }; -} - -// ============================================================================ -// HELPER FUNCTIONS -// ============================================================================ - -/** - * Preview what connections would be discovered without creating them - * Useful for testing or showing users potential discoveries - */ -export async function previewREMCycleJob( - db: VestigeDatabase, - maxAnalyze: number = 100 -): Promise { - const cycleResult = await runREMCycle(db, { - maxAnalyze, - dryRun: true, - }); - - return { - nodesAnalyzed: cycleResult.nodesAnalyzed, - connectionsDiscovered: cycleResult.connectionsDiscovered, - connectionsCreated: 0, - duration: cycleResult.duration, - discoveries: cycleResult.discoveries.map(d => ({ - nodeA: d.nodeA, - nodeB: d.nodeB, - reason: d.reason, - })), - timestamp: new Date(), - }; -} diff --git a/packages/core/src/jobs/index.ts b/packages/core/src/jobs/index.ts deleted file mode 100644 index 27fb5d9..0000000 --- a/packages/core/src/jobs/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Jobs Module - Background Job Processing for Vestige MCP - * - * This module provides a production-ready job queue system with: - * - Priority-based scheduling - * - Retry logic with exponential backoff - * - Concurrency control - * - Cron-like recurring job scheduling - * - Event-driven architecture - * - * @module jobs - * - * @example - * ```typescript - * import { - * JobQueue, - * createDecayJobHandler, - * createREMCycleJobHandler, - * createConsolidationJobHandler, - * } from './jobs'; - * import { VestigeDatabase } from './core'; - * - * // Initialize - * const db = new VestigeDatabase(); - * const queue = new JobQueue(); - * - * // Register job handlers - * queue.register('decay', createDecayJobHandler(db), { concurrency: 1 }); - * queue.register('rem-cycle', createREMCycleJobHandler(db), { concurrency: 1 }); - * queue.register('consolidation', createConsolidationJobHandler(db), { concurrency: 1 }); - * - * // Schedule recurring jobs - * queue.schedule('decay', '0 3 * * *', {}); // Daily at 3 AM - * queue.schedule('rem-cycle', '0 *\/6 * * *', {}); // Every 6 hours - * queue.schedule('consolidation', '0 4 * * 0', {}); // Weekly on Sunday at 4 AM - * - * // Start processing - * queue.start(); - * - * // Listen to events - * queue.on('job:completed', (job, result) => { - * console.log(`Job ${job.name} completed:`, result); - * }); - * - * queue.on('job:failed', (job, error) => { - * console.error(`Job ${job.name} failed:`, error); - * }); - * - * // Add one-off jobs - * queue.add('rem-cycle', { maxAnalyze: 200 }, { priority: 10 }); - * - * // Graceful shutdown - * process.on('SIGTERM', async () => { - * await queue.shutdown(); - * db.close(); - * }); - * ``` - */ - -// Core job queue -export { - JobQueue, - getDefaultQueue, - resetDefaultQueue, - type Job, - type JobResult, - type JobHandler, - type JobOptions, - type JobDefinition, - type JobStatus, - type ScheduledJob, - type QueueStats, - type JobQueueEvents, -} from './JobQueue.js'; - -// Decay job -export { - createDecayJobHandler, - getCriticallyDecayedNodes, - type DecayJobData, - type DecayJobResult, -} from './DecayJob.js'; - -// REM cycle job -export { - createREMCycleJobHandler, - previewREMCycleJob, - type REMCycleJobData, - type REMCycleJobResult, -} from './REMCycleJob.js'; - -// Consolidation job -export { - createConsolidationJobHandler, - previewConsolidation, - getConsolidationMetrics, - type ConsolidationJobData, - type ConsolidationJobResult, -} from './ConsolidationJob.js'; diff --git a/packages/core/src/repositories/EdgeRepository.ts b/packages/core/src/repositories/EdgeRepository.ts deleted file mode 100644 index 126f6e9..0000000 --- a/packages/core/src/repositories/EdgeRepository.ts +++ /dev/null @@ -1,659 +0,0 @@ -import Database from 'better-sqlite3'; -import { nanoid } from 'nanoid'; -import type { GraphEdge } from '../core/types.js'; - -// ============================================================================ -// EDGE TYPES -// ============================================================================ - -export type EdgeType = - | 'relates_to' - | 'contradicts' - | 'supports' - | 'similar_to' - | 'part_of' - | 'caused_by' - | 'mentions' - | 'derived_from' - | 'references' - | 'follows' - | 'person_mentioned' - | 'concept_instance'; - -// ============================================================================ -// INPUT TYPES -// ============================================================================ - -export interface GraphEdgeInput { - fromId: string; - toId: string; - edgeType: EdgeType; - weight?: number; - metadata?: Record; -} - -// ============================================================================ -// TRANSITIVE PATH TYPE -// ============================================================================ - -export interface TransitivePath { - path: string[]; - totalWeight: number; -} - -// ============================================================================ -// RWLOCK - Read-Write Lock for concurrent access control -// ============================================================================ - -/** - * A simple read-write lock implementation. - * - Multiple readers can hold the lock concurrently - * - Writers have exclusive access - * - Writers wait for all readers to release - * - Readers wait if a writer is active or waiting - */ -export class RWLock { - private readers = 0; - private writer = false; - private writerQueue: (() => void)[] = []; - private readerQueue: (() => void)[] = []; - - async acquireRead(): Promise { - return new Promise((resolve) => { - if (!this.writer && this.writerQueue.length === 0) { - this.readers++; - resolve(); - } else { - this.readerQueue.push(() => { - this.readers++; - resolve(); - }); - } - }); - } - - releaseRead(): void { - this.readers--; - if (this.readers === 0 && this.writerQueue.length > 0) { - this.writer = true; - const next = this.writerQueue.shift(); - if (next) next(); - } - } - - async acquireWrite(): Promise { - return new Promise((resolve) => { - if (!this.writer && this.readers === 0) { - this.writer = true; - resolve(); - } else { - this.writerQueue.push(resolve); - } - }); - } - - releaseWrite(): void { - this.writer = false; - // Prefer waiting readers over writers to prevent writer starvation - if (this.readerQueue.length > 0) { - const readers = this.readerQueue.splice(0); - for (const reader of readers) { - reader(); - } - } else if (this.writerQueue.length > 0) { - this.writer = true; - const next = this.writerQueue.shift(); - if (next) next(); - } - } - - /** - * Execute a function with read lock - */ - async withRead(fn: () => T | Promise): Promise { - await this.acquireRead(); - try { - return await fn(); - } finally { - this.releaseRead(); - } - } - - /** - * Execute a function with write lock - */ - async withWrite(fn: () => T | Promise): Promise { - await this.acquireWrite(); - try { - return await fn(); - } finally { - this.releaseWrite(); - } - } -} - -// ============================================================================ -// INTERFACE -// ============================================================================ - -export interface IEdgeRepository { - create(input: GraphEdgeInput): Promise; - findById(id: string): Promise; - findByNodes(fromId: string, toId: string, edgeType?: string): Promise; - delete(id: string): Promise; - deleteByNodes(fromId: string, toId: string): Promise; - getEdgesFrom(nodeId: string): Promise; - getEdgesTo(nodeId: string): Promise; - getAllEdges(nodeId: string): Promise; - getRelatedNodeIds(nodeId: string, depth?: number): Promise; - updateWeight(id: string, weight: number): Promise; - strengthenEdge(id: string, boost: number): Promise; - pruneWeakEdges(threshold: number): Promise; - getTransitivePaths(nodeId: string, maxDepth: number): Promise; - strengthenConnectedEdges(nodeId: string, boost: number): Promise; -} - -// ============================================================================ -// ERROR CLASS -// ============================================================================ - -/** - * Sanitize error message to prevent sensitive data leakage - */ -function sanitizeErrorMessage(message: string): string { - let sanitized = message.replace(/\/[^\s]+/g, '[PATH]'); - sanitized = sanitized.replace(/SELECT|INSERT|UPDATE|DELETE|DROP|CREATE/gi, '[SQL]'); - sanitized = sanitized.replace(/\b(password|secret|key|token|auth)\s*[=:]\s*\S+/gi, '[REDACTED]'); - return sanitized; -} - -export class EdgeRepositoryError extends Error { - constructor( - message: string, - public readonly code: string, - cause?: unknown - ) { - super(sanitizeErrorMessage(message)); - this.name = 'EdgeRepositoryError'; - if (process.env['NODE_ENV'] === 'development' && cause) { - this.cause = cause; - } - } -} - -// ============================================================================ -// IMPLEMENTATION -// ============================================================================ - -export class EdgeRepository implements IEdgeRepository { - private readonly lock = new RWLock(); - - constructor(private readonly db: Database.Database) {} - - /** - * Create a new edge between two nodes. - * Handles UNIQUE constraint gracefully by using INSERT OR REPLACE. - */ - async create(input: GraphEdgeInput): Promise { - return this.lock.withWrite(() => { - try { - const id = nanoid(); - const now = new Date().toISOString(); - const weight = input.weight ?? 0.5; - - // Check if edge already exists - const existing = this.db.prepare(` - SELECT id FROM graph_edges - WHERE from_id = ? AND to_id = ? AND edge_type = ? - `).get(input.fromId, input.toId, input.edgeType) as { id: string } | undefined; - - if (existing) { - // Update existing edge - boost weight slightly - const updateStmt = this.db.prepare(` - UPDATE graph_edges - SET weight = MIN(1.0, weight + ?), - metadata = ? - WHERE id = ? - `); - updateStmt.run(weight * 0.1, JSON.stringify(input.metadata || {}), existing.id); - - // Return the updated edge - const row = this.db.prepare('SELECT * FROM graph_edges WHERE id = ?') - .get(existing.id) as Record; - return this.rowToEdge(row); - } - - // Insert new edge - const stmt = this.db.prepare(` - INSERT INTO graph_edges ( - id, from_id, to_id, edge_type, weight, metadata, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, - input.fromId, - input.toId, - input.edgeType, - weight, - JSON.stringify(input.metadata || {}), - now - ); - - return { - id, - fromId: input.fromId, - toId: input.toId, - edgeType: input.edgeType as GraphEdge['edgeType'], - weight, - metadata: input.metadata || {}, - createdAt: new Date(now), - }; - } catch (error) { - throw new EdgeRepositoryError( - 'Failed to create edge', - 'CREATE_EDGE_FAILED', - error - ); - } - }); - } - - /** - * Find an edge by its ID. - */ - async findById(id: string): Promise { - return this.lock.withRead(() => { - try { - const stmt = this.db.prepare('SELECT * FROM graph_edges WHERE id = ?'); - const row = stmt.get(id) as Record | undefined; - if (!row) return null; - return this.rowToEdge(row); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to find edge: ${id}`, - 'FIND_EDGE_FAILED', - error - ); - } - }); - } - - /** - * Find an edge by its source and target nodes. - * Optionally filter by edge type. - */ - async findByNodes(fromId: string, toId: string, edgeType?: string): Promise { - return this.lock.withRead(() => { - try { - let stmt; - let row: Record | undefined; - - if (edgeType) { - stmt = this.db.prepare(` - SELECT * FROM graph_edges - WHERE from_id = ? AND to_id = ? AND edge_type = ? - `); - row = stmt.get(fromId, toId, edgeType) as Record | undefined; - } else { - stmt = this.db.prepare(` - SELECT * FROM graph_edges - WHERE from_id = ? AND to_id = ? - `); - row = stmt.get(fromId, toId) as Record | undefined; - } - - if (!row) return null; - return this.rowToEdge(row); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to find edge by nodes`, - 'FIND_BY_NODES_FAILED', - error - ); - } - }); - } - - /** - * Delete an edge by its ID. - */ - async delete(id: string): Promise { - return this.lock.withWrite(() => { - try { - const stmt = this.db.prepare('DELETE FROM graph_edges WHERE id = ?'); - const result = stmt.run(id); - return result.changes > 0; - } catch (error) { - throw new EdgeRepositoryError( - `Failed to delete edge: ${id}`, - 'DELETE_EDGE_FAILED', - error - ); - } - }); - } - - /** - * Delete all edges between two nodes (in both directions). - */ - async deleteByNodes(fromId: string, toId: string): Promise { - return this.lock.withWrite(() => { - try { - const stmt = this.db.prepare(` - DELETE FROM graph_edges - WHERE (from_id = ? AND to_id = ?) OR (from_id = ? AND to_id = ?) - `); - const result = stmt.run(fromId, toId, toId, fromId); - return result.changes > 0; - } catch (error) { - throw new EdgeRepositoryError( - `Failed to delete edges between nodes`, - 'DELETE_BY_NODES_FAILED', - error - ); - } - }); - } - - /** - * Get all edges originating from a node. - */ - async getEdgesFrom(nodeId: string): Promise { - return this.lock.withRead(() => { - try { - const stmt = this.db.prepare('SELECT * FROM graph_edges WHERE from_id = ?'); - const rows = stmt.all(nodeId) as Record[]; - return rows.map(row => this.rowToEdge(row)); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to get edges from node: ${nodeId}`, - 'GET_EDGES_FROM_FAILED', - error - ); - } - }); - } - - /** - * Get all edges pointing to a node. - */ - async getEdgesTo(nodeId: string): Promise { - return this.lock.withRead(() => { - try { - const stmt = this.db.prepare('SELECT * FROM graph_edges WHERE to_id = ?'); - const rows = stmt.all(nodeId) as Record[]; - return rows.map(row => this.rowToEdge(row)); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to get edges to node: ${nodeId}`, - 'GET_EDGES_TO_FAILED', - error - ); - } - }); - } - - /** - * Get all edges connected to a node (both incoming and outgoing). - */ - async getAllEdges(nodeId: string): Promise { - return this.lock.withRead(() => { - try { - const stmt = this.db.prepare(` - SELECT * FROM graph_edges - WHERE from_id = ? OR to_id = ? - `); - const rows = stmt.all(nodeId, nodeId) as Record[]; - return rows.map(row => this.rowToEdge(row)); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to get all edges for node: ${nodeId}`, - 'GET_ALL_EDGES_FAILED', - error - ); - } - }); - } - - /** - * Get related node IDs using BFS traversal. - * Extracted from database.ts getRelatedNodes(). - */ - async getRelatedNodeIds(nodeId: string, depth: number = 1): Promise { - return this.lock.withRead(() => { - try { - const visited = new Set(); - let current = [nodeId]; - - for (let d = 0; d < depth; d++) { - if (current.length === 0) break; - - const placeholders = current.map(() => '?').join(','); - const stmt = this.db.prepare(` - SELECT DISTINCT - CASE WHEN from_id IN (${placeholders}) THEN to_id ELSE from_id END as related_id - FROM graph_edges - WHERE from_id IN (${placeholders}) OR to_id IN (${placeholders}) - `); - - const params = [...current, ...current, ...current]; - const rows = stmt.all(...params) as { related_id: string }[]; - - const newNodes: string[] = []; - for (const row of rows) { - if (!visited.has(row.related_id) && row.related_id !== nodeId) { - visited.add(row.related_id); - newNodes.push(row.related_id); - } - } - current = newNodes; - } - - return Array.from(visited); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to get related nodes: ${nodeId}`, - 'GET_RELATED_FAILED', - error - ); - } - }); - } - - /** - * Update the weight of an edge. - */ - async updateWeight(id: string, weight: number): Promise { - return this.lock.withWrite(() => { - try { - // Clamp weight to valid range - const clampedWeight = Math.max(0, Math.min(1, weight)); - - const stmt = this.db.prepare(` - UPDATE graph_edges SET weight = ? WHERE id = ? - `); - stmt.run(clampedWeight, id); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to update edge weight: ${id}`, - 'UPDATE_WEIGHT_FAILED', - error - ); - } - }); - } - - /** - * Strengthen an edge by boosting its weight. - * Used for spreading activation. - */ - async strengthenEdge(id: string, boost: number): Promise { - return this.lock.withWrite(() => { - try { - // Ensure boost is positive and reasonable - const safeBoost = Math.max(0, Math.min(0.5, boost)); - - const stmt = this.db.prepare(` - UPDATE graph_edges - SET weight = MIN(1.0, weight + ?) - WHERE id = ? - `); - stmt.run(safeBoost, id); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to strengthen edge: ${id}`, - 'STRENGTHEN_EDGE_FAILED', - error - ); - } - }); - } - - /** - * Prune edges with weight below a threshold. - * Returns the number of edges removed. - */ - async pruneWeakEdges(threshold: number): Promise { - return this.lock.withWrite(() => { - try { - // Validate threshold - const safeThreshold = Math.max(0, Math.min(1, threshold)); - - const stmt = this.db.prepare(` - DELETE FROM graph_edges WHERE weight < ? - `); - const result = stmt.run(safeThreshold); - return result.changes; - } catch (error) { - throw new EdgeRepositoryError( - 'Failed to prune weak edges', - 'PRUNE_EDGES_FAILED', - error - ); - } - }); - } - - /** - * Get all transitive paths from a node up to maxDepth. - * Used for spreading activation in graph traversal. - */ - async getTransitivePaths(nodeId: string, maxDepth: number): Promise { - return this.lock.withRead(() => { - try { - const paths: TransitivePath[] = []; - const visited = new Set(); - - // BFS with path tracking - interface QueueItem { - nodeId: string; - path: string[]; - totalWeight: number; - } - - const queue: QueueItem[] = [{ nodeId, path: [nodeId], totalWeight: 1.0 }]; - visited.add(nodeId); - - while (queue.length > 0) { - const current = queue.shift()!; - - if (current.path.length > maxDepth + 1) continue; - - // Get all connected edges - const stmt = this.db.prepare(` - SELECT to_id, from_id, weight FROM graph_edges - WHERE from_id = ? OR to_id = ? - `); - const edges = stmt.all(current.nodeId, current.nodeId) as { - to_id: string; - from_id: string; - weight: number; - }[]; - - for (const edge of edges) { - const nextNode = edge.from_id === current.nodeId ? edge.to_id : edge.from_id; - - if (!visited.has(nextNode)) { - visited.add(nextNode); - const newPath = [...current.path, nextNode]; - const newWeight = current.totalWeight * edge.weight; - - paths.push({ path: newPath, totalWeight: newWeight }); - - if (newPath.length <= maxDepth) { - queue.push({ - nodeId: nextNode, - path: newPath, - totalWeight: newWeight, - }); - } - } - } - } - - // Sort by total weight (descending) for relevance - return paths.sort((a, b) => b.totalWeight - a.totalWeight); - } catch (error) { - throw new EdgeRepositoryError( - `Failed to get transitive paths: ${nodeId}`, - 'GET_PATHS_FAILED', - error - ); - } - }); - } - - /** - * Strengthen all edges connected to a node. - * Used for memory reconsolidation. - * Returns the number of edges strengthened. - */ - async strengthenConnectedEdges(nodeId: string, boost: number): Promise { - return this.lock.withWrite(() => { - try { - // Ensure boost is positive and reasonable - const safeBoost = Math.max(0, Math.min(0.5, boost)); - - const stmt = this.db.prepare(` - UPDATE graph_edges - SET weight = MIN(1.0, weight + ?) - WHERE from_id = ? OR to_id = ? - `); - const result = stmt.run(safeBoost, nodeId, nodeId); - return result.changes; - } catch (error) { - throw new EdgeRepositoryError( - `Failed to strengthen connected edges: ${nodeId}`, - 'STRENGTHEN_CONNECTED_FAILED', - error - ); - } - }); - } - - // ============================================================================ - // HELPERS - // ============================================================================ - - private rowToEdge(row: Record): GraphEdge { - return { - id: row['id'] as string, - fromId: row['from_id'] as string, - toId: row['to_id'] as string, - edgeType: row['edge_type'] as GraphEdge['edgeType'], - weight: row['weight'] as number, - metadata: this.safeJsonParse(row['metadata'] as string, {}), - createdAt: new Date(row['created_at'] as string), - }; - } - - private safeJsonParse(value: string | null | undefined, fallback: T): T { - if (!value) return fallback; - try { - return JSON.parse(value) as T; - } catch { - return fallback; - } - } -} diff --git a/packages/core/src/repositories/NodeRepository.ts b/packages/core/src/repositories/NodeRepository.ts deleted file mode 100644 index edebfd6..0000000 --- a/packages/core/src/repositories/NodeRepository.ts +++ /dev/null @@ -1,879 +0,0 @@ -/** - * NodeRepository - Repository for knowledge node operations - * - * Extracted from the monolithic database.ts to provide a focused, testable - * interface for node CRUD operations with proper concurrency control. - */ - -import type Database from 'better-sqlite3'; -import { nanoid } from 'nanoid'; -import type { - KnowledgeNode, - KnowledgeNodeInput, -} from '../core/types.js'; -import { RWLock } from '../utils/mutex.js'; -import { safeJsonParse } from '../utils/json.js'; -import { NotFoundError, ValidationError, DatabaseError } from '../core/errors.js'; -import { analyzeSentimentIntensity, captureGitContext } from '../core/database.js'; - -// ============================================================================ -// CONSTANTS -// ============================================================================ - -const DEFAULT_LIMIT = 50; -const MAX_LIMIT = 500; - -// Input validation limits -const MAX_CONTENT_LENGTH = 1_000_000; // 1MB max content -const MAX_QUERY_LENGTH = 10_000; // 10KB max query -const MAX_TAGS_COUNT = 100; // Max tags per node - -// SM-2 Spaced Repetition Constants -const SM2_EASE_FACTOR = 2.5; -const SM2_LAPSE_THRESHOLD = 0.3; -const SM2_MIN_STABILITY = 1.0; -const SM2_MAX_STABILITY = 365.0; - -// Sentiment-Weighted Decay Constants -const SENTIMENT_STABILITY_BOOST = 2.0; -const SENTIMENT_MIN_BOOST = 1.0; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface PaginationOptions { - limit?: number; - offset?: number; -} - -export interface PaginatedResult { - items: T[]; - total: number; - limit: number; - offset: number; - hasMore: boolean; -} - -export interface GitContext { - branch?: string; - commit?: string; - commitMessage?: string; - repoPath?: string; - dirty?: boolean; - changedFiles?: string[]; -} - -// ============================================================================ -// INTERFACE -// ============================================================================ - -export interface INodeRepository { - findById(id: string): Promise; - findByIds(ids: string[]): Promise; - create(input: KnowledgeNodeInput): Promise; - update(id: string, updates: Partial): Promise; - delete(id: string): Promise; - search(query: string, options?: PaginationOptions): Promise>; - getRecent(options?: PaginationOptions): Promise>; - getDecaying(threshold: number, options?: PaginationOptions): Promise>; - getDueForReview(options?: PaginationOptions): Promise>; - recordAccess(id: string): Promise; - markReviewed(id: string): Promise; - applyDecay(id: string): Promise; - applyDecayAll(): Promise; - findByTag(tag: string, options?: PaginationOptions): Promise>; - findByPerson(personName: string, options?: PaginationOptions): Promise>; -} - -// ============================================================================ -// VALIDATION HELPERS -// ============================================================================ - -/** - * Validate string length for inputs - */ -function validateStringLength(value: string, maxLength: number, fieldName: string): void { - if (value && value.length > maxLength) { - throw new ValidationError( - `${fieldName} exceeds maximum length of ${maxLength} characters`, - { field: fieldName.toLowerCase(), maxLength, actualLength: value.length } - ); - } -} - -/** - * Validate array length for inputs - */ -function validateArrayLength(arr: T[] | undefined, maxLength: number, fieldName: string): void { - if (arr && arr.length > maxLength) { - throw new ValidationError( - `${fieldName} exceeds maximum count of ${maxLength} items`, - { field: fieldName.toLowerCase(), maxLength, actualLength: arr.length } - ); - } -} - -/** - * Normalize pagination options - */ -function normalizePagination(options: PaginationOptions = {}): { limit: number; offset: number } { - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - return { - limit: Math.min(Math.max(1, limit), MAX_LIMIT), - offset: Math.max(0, offset), - }; -} - -// ============================================================================ -// IMPLEMENTATION -// ============================================================================ - -export class NodeRepository implements INodeRepository { - private readonly lock = new RWLock(); - - constructor(private readonly db: Database.Database) {} - - // -------------------------------------------------------------------------- - // READ OPERATIONS - // -------------------------------------------------------------------------- - - async findById(id: string): Promise { - return this.lock.withReadLock(async () => { - try { - const stmt = this.db.prepare('SELECT * FROM knowledge_nodes WHERE id = ?'); - const row = stmt.get(id) as Record | undefined; - if (!row) return null; - return this.rowToEntity(row); - } catch (error) { - throw new DatabaseError(`Failed to get node: ${id}`, error); - } - }); - } - - async findByIds(ids: string[]): Promise { - if (ids.length === 0) return []; - - return this.lock.withReadLock(async () => { - try { - const placeholders = ids.map(() => '?').join(','); - const stmt = this.db.prepare( - `SELECT * FROM knowledge_nodes WHERE id IN (${placeholders})` - ); - const rows = stmt.all(...ids) as Record[]; - return rows.map((row) => this.rowToEntity(row)); - } catch (error) { - throw new DatabaseError('Failed to get nodes by IDs', error); - } - }); - } - - async search(query: string, options: PaginationOptions = {}): Promise> { - return this.lock.withReadLock(async () => { - try { - // Input validation - validateStringLength(query, MAX_QUERY_LENGTH, 'Search query'); - - // Sanitize FTS5 query to prevent injection - const sanitizedQuery = query - .replace(/[^\w\s\-]/g, ' ') - .trim(); - - if (!sanitizedQuery) { - return { - items: [], - total: 0, - limit: DEFAULT_LIMIT, - offset: 0, - hasMore: false, - }; - } - - const { limit, offset } = normalizePagination(options); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM knowledge_nodes kn - JOIN knowledge_fts fts ON kn.id = fts.id - WHERE knowledge_fts MATCH ? - `); - const countResult = countStmt.get(sanitizedQuery) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT kn.* FROM knowledge_nodes kn - JOIN knowledge_fts fts ON kn.id = fts.id - WHERE knowledge_fts MATCH ? - ORDER BY rank - LIMIT ? OFFSET ? - `); - const rows = stmt.all(sanitizedQuery, limit, offset) as Record[]; - const items = rows.map((row) => this.rowToEntity(row)); - - return { - items, - total, - limit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - if (error instanceof ValidationError) throw error; - throw new DatabaseError('Search operation failed', error); - } - }); - } - - async getRecent(options: PaginationOptions = {}): Promise> { - return this.lock.withReadLock(async () => { - try { - const { limit, offset } = normalizePagination(options); - - // Get total count - const countResult = this.db.prepare('SELECT COUNT(*) as total FROM knowledge_nodes').get() as { - total: number; - }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM knowledge_nodes - ORDER BY created_at DESC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(limit, offset) as Record[]; - const items = rows.map((row) => this.rowToEntity(row)); - - return { - items, - total, - limit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new DatabaseError('Failed to get recent nodes', error); - } - }); - } - - async getDecaying( - threshold: number = 0.5, - options: PaginationOptions = {} - ): Promise> { - return this.lock.withReadLock(async () => { - try { - const { limit, offset } = normalizePagination(options); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM knowledge_nodes - WHERE retention_strength < ? - `); - const countResult = countStmt.get(threshold) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM knowledge_nodes - WHERE retention_strength < ? - ORDER BY retention_strength ASC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(threshold, limit, offset) as Record[]; - const items = rows.map((row) => this.rowToEntity(row)); - - return { - items, - total, - limit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new DatabaseError('Failed to get decaying nodes', error); - } - }); - } - - async getDueForReview(options: PaginationOptions = {}): Promise> { - return this.lock.withReadLock(async () => { - try { - const { limit, offset } = normalizePagination(options); - const now = new Date().toISOString(); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM knowledge_nodes - WHERE next_review_date IS NOT NULL AND next_review_date <= ? - `); - const countResult = countStmt.get(now) as { total: number }; - const total = countResult.total; - - // Get paginated results, ordered by retention strength (most urgent first) - const stmt = this.db.prepare(` - SELECT * FROM knowledge_nodes - WHERE next_review_date IS NOT NULL AND next_review_date <= ? - ORDER BY retention_strength ASC, next_review_date ASC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(now, limit, offset) as Record[]; - const items = rows.map((row) => this.rowToEntity(row)); - - return { - items, - total, - limit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new DatabaseError('Failed to get nodes due for review', error); - } - }); - } - - async findByTag(tag: string, options: PaginationOptions = {}): Promise> { - return this.lock.withReadLock(async () => { - try { - const { limit, offset } = normalizePagination(options); - - // Escape special JSON/LIKE characters - const escapedTag = tag - .replace(/\\/g, '\\\\') - .replace(/%/g, '\\%') - .replace(/_/g, '\\_') - .replace(/"/g, '\\"'); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM knowledge_nodes - WHERE tags LIKE ? ESCAPE '\\' - `); - const countResult = countStmt.get(`%"${escapedTag}"%`) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM knowledge_nodes - WHERE tags LIKE ? ESCAPE '\\' - ORDER BY created_at DESC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(`%"${escapedTag}"%`, limit, offset) as Record[]; - const items = rows.map((row) => this.rowToEntity(row)); - - return { - items, - total, - limit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new DatabaseError('Failed to find nodes by tag', error); - } - }); - } - - async findByPerson( - personName: string, - options: PaginationOptions = {} - ): Promise> { - return this.lock.withReadLock(async () => { - try { - const { limit, offset } = normalizePagination(options); - - // Escape special JSON/LIKE characters - const escapedPerson = personName - .replace(/\\/g, '\\\\') - .replace(/%/g, '\\%') - .replace(/_/g, '\\_') - .replace(/"/g, '\\"'); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM knowledge_nodes - WHERE people LIKE ? ESCAPE '\\' - `); - const countResult = countStmt.get(`%"${escapedPerson}"%`) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM knowledge_nodes - WHERE people LIKE ? ESCAPE '\\' - ORDER BY created_at DESC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(`%"${escapedPerson}"%`, limit, offset) as Record[]; - const items = rows.map((row) => this.rowToEntity(row)); - - return { - items, - total, - limit, - offset, - hasMore: offset + items.length < total, - }; - } catch (error) { - throw new DatabaseError('Failed to find nodes by person', error); - } - }); - } - - // -------------------------------------------------------------------------- - // WRITE OPERATIONS - // -------------------------------------------------------------------------- - - async create(input: KnowledgeNodeInput): Promise { - return this.lock.withWriteLock(async () => { - try { - // Input validation - validateStringLength(input.content, MAX_CONTENT_LENGTH, 'Content'); - validateStringLength(input.summary || '', MAX_CONTENT_LENGTH, 'Summary'); - validateArrayLength(input.tags, MAX_TAGS_COUNT, 'Tags'); - validateArrayLength(input.people, MAX_TAGS_COUNT, 'People'); - validateArrayLength(input.concepts, MAX_TAGS_COUNT, 'Concepts'); - validateArrayLength(input.events, MAX_TAGS_COUNT, 'Events'); - - // Validate confidence is within bounds - const confidence = Math.max(0, Math.min(1, input.confidence ?? 0.8)); - const retention = Math.max(0, Math.min(1, input.retentionStrength ?? 1.0)); - - // Analyze emotional intensity of content - const sentimentIntensity = - input.sentimentIntensity ?? analyzeSentimentIntensity(input.content); - - // Git-Blame for Thoughts: Capture current code context - const gitContext = input.gitContext ?? captureGitContext(); - - const id = nanoid(); - const now = new Date().toISOString(); - - const stmt = this.db.prepare(` - INSERT INTO knowledge_nodes ( - id, content, summary, - created_at, updated_at, last_accessed_at, access_count, - retention_strength, sentiment_intensity, next_review_date, review_count, - source_type, source_platform, source_id, source_url, source_chain, git_context, - confidence, is_contradicted, contradiction_ids, - people, concepts, events, tags - ) VALUES ( - ?, ?, ?, - ?, ?, ?, ?, - ?, ?, ?, ?, - ?, ?, ?, ?, ?, ?, - ?, ?, ?, - ?, ?, ?, ? - ) - `); - - const createdAt = input.createdAt instanceof Date - ? input.createdAt.toISOString() - : (input.createdAt || now); - - stmt.run( - id, - input.content, - input.summary || null, - createdAt, - now, - now, - 0, - retention, - sentimentIntensity, - input.nextReviewDate instanceof Date - ? input.nextReviewDate.toISOString() - : (input.nextReviewDate || null), - 0, - input.sourceType, - input.sourcePlatform, - input.sourceId || null, - input.sourceUrl || null, - JSON.stringify(input.sourceChain || []), - gitContext ? JSON.stringify(gitContext) : null, - confidence, - input.isContradicted ? 1 : 0, - JSON.stringify(input.contradictionIds || []), - JSON.stringify(input.people || []), - JSON.stringify(input.concepts || []), - JSON.stringify(input.events || []), - JSON.stringify(input.tags || []) - ); - - // Return the created node - const node = await this.findById(id); - if (!node) { - throw new DatabaseError('Failed to retrieve created node'); - } - return node; - } catch (error) { - if (error instanceof ValidationError || error instanceof DatabaseError) throw error; - throw new DatabaseError('Failed to insert knowledge node', error); - } - }); - } - - async update(id: string, updates: Partial): Promise { - return this.lock.withWriteLock(async () => { - try { - // Check if node exists - const existing = this.db.prepare('SELECT * FROM knowledge_nodes WHERE id = ?').get(id); - if (!existing) { - return null; - } - - // Input validation - if (updates.content !== undefined) { - validateStringLength(updates.content, MAX_CONTENT_LENGTH, 'Content'); - } - if (updates.summary !== undefined) { - validateStringLength(updates.summary, MAX_CONTENT_LENGTH, 'Summary'); - } - if (updates.tags !== undefined) { - validateArrayLength(updates.tags, MAX_TAGS_COUNT, 'Tags'); - } - if (updates.people !== undefined) { - validateArrayLength(updates.people, MAX_TAGS_COUNT, 'People'); - } - if (updates.concepts !== undefined) { - validateArrayLength(updates.concepts, MAX_TAGS_COUNT, 'Concepts'); - } - if (updates.events !== undefined) { - validateArrayLength(updates.events, MAX_TAGS_COUNT, 'Events'); - } - - // Build dynamic update - const setClauses: string[] = []; - const values: unknown[] = []; - - if (updates.content !== undefined) { - setClauses.push('content = ?'); - values.push(updates.content); - - // Re-analyze sentiment when content changes - const sentimentIntensity = analyzeSentimentIntensity(updates.content); - setClauses.push('sentiment_intensity = ?'); - values.push(sentimentIntensity); - } - - if (updates.summary !== undefined) { - setClauses.push('summary = ?'); - values.push(updates.summary); - } - - if (updates.confidence !== undefined) { - setClauses.push('confidence = ?'); - values.push(Math.max(0, Math.min(1, updates.confidence))); - } - - if (updates.retentionStrength !== undefined) { - setClauses.push('retention_strength = ?'); - values.push(Math.max(0, Math.min(1, updates.retentionStrength))); - } - - if (updates.tags !== undefined) { - setClauses.push('tags = ?'); - values.push(JSON.stringify(updates.tags)); - } - - if (updates.people !== undefined) { - setClauses.push('people = ?'); - values.push(JSON.stringify(updates.people)); - } - - if (updates.concepts !== undefined) { - setClauses.push('concepts = ?'); - values.push(JSON.stringify(updates.concepts)); - } - - if (updates.events !== undefined) { - setClauses.push('events = ?'); - values.push(JSON.stringify(updates.events)); - } - - if (updates.isContradicted !== undefined) { - setClauses.push('is_contradicted = ?'); - values.push(updates.isContradicted ? 1 : 0); - } - - if (updates.contradictionIds !== undefined) { - setClauses.push('contradiction_ids = ?'); - values.push(JSON.stringify(updates.contradictionIds)); - } - - if (setClauses.length === 0) { - // No updates to make, just return existing node - return this.rowToEntity(existing as Record); - } - - // Always update updated_at - setClauses.push('updated_at = ?'); - values.push(new Date().toISOString()); - - // Add the ID for the WHERE clause - values.push(id); - - const sql = `UPDATE knowledge_nodes SET ${setClauses.join(', ')} WHERE id = ?`; - this.db.prepare(sql).run(...values); - - // Return updated node - const updated = this.db.prepare('SELECT * FROM knowledge_nodes WHERE id = ?').get(id); - return updated ? this.rowToEntity(updated as Record) : null; - } catch (error) { - if (error instanceof ValidationError || error instanceof DatabaseError) throw error; - throw new DatabaseError(`Failed to update node: ${id}`, error); - } - }); - } - - async delete(id: string): Promise { - return this.lock.withWriteLock(async () => { - try { - const stmt = this.db.prepare('DELETE FROM knowledge_nodes WHERE id = ?'); - const result = stmt.run(id); - return result.changes > 0; - } catch (error) { - throw new DatabaseError(`Failed to delete node: ${id}`, error); - } - }); - } - - async recordAccess(id: string): Promise { - return this.lock.withWriteLock(async () => { - try { - const stmt = this.db.prepare(` - UPDATE knowledge_nodes - SET last_accessed_at = ?, access_count = access_count + 1 - WHERE id = ? - `); - stmt.run(new Date().toISOString(), id); - } catch (error) { - throw new DatabaseError(`Failed to record access: ${id}`, error); - } - }); - } - - async markReviewed(id: string): Promise { - return this.lock.withWriteLock(async () => { - try { - // Get the node first - const nodeStmt = this.db.prepare('SELECT * FROM knowledge_nodes WHERE id = ?'); - const nodeRow = nodeStmt.get(id) as Record | undefined; - - if (!nodeRow) { - throw new NotFoundError('KnowledgeNode', id); - } - - const node = this.rowToEntity(nodeRow); - const currentStability = node.stabilityFactor ?? SM2_MIN_STABILITY; - let newStability: number; - let newReviewCount: number; - - // SM-2 with Lapse Detection - if (node.retentionStrength >= SM2_LAPSE_THRESHOLD) { - // SUCCESSFUL RECALL: Memory was still accessible - newStability = Math.min(SM2_MAX_STABILITY, currentStability * SM2_EASE_FACTOR); - newReviewCount = node.reviewCount + 1; - } else { - // LAPSE: Memory had decayed too far - newStability = SM2_MIN_STABILITY; - newReviewCount = node.reviewCount + 1; - } - - // Reset retention to full strength - const newRetention = 1.0; - - // Calculate next review date - const daysUntilReview = Math.ceil(newStability); - const nextReview = new Date(); - nextReview.setDate(nextReview.getDate() + daysUntilReview); - - const updateStmt = this.db.prepare(` - UPDATE knowledge_nodes - SET retention_strength = ?, - stability_factor = ?, - review_count = ?, - next_review_date = ?, - last_accessed_at = ?, - updated_at = ? - WHERE id = ? - `); - const now = new Date().toISOString(); - updateStmt.run( - newRetention, - newStability, - newReviewCount, - nextReview.toISOString(), - now, - now, - id - ); - - // Return the updated node - const updatedRow = nodeStmt.get(id) as Record; - return this.rowToEntity(updatedRow); - } catch (error) { - if (error instanceof NotFoundError) throw error; - throw new DatabaseError('Failed to mark node as reviewed', error); - } - }); - } - - async applyDecay(id: string): Promise { - return this.lock.withWriteLock(async () => { - try { - const nodeStmt = this.db.prepare(` - SELECT id, last_accessed_at, retention_strength, stability_factor, sentiment_intensity - FROM knowledge_nodes WHERE id = ? - `); - const node = nodeStmt.get(id) as { - id: string; - last_accessed_at: string; - retention_strength: number; - stability_factor: number | null; - sentiment_intensity: number | null; - } | undefined; - - if (!node) { - throw new NotFoundError('KnowledgeNode', id); - } - - const now = Date.now(); - const lastAccessed = new Date(node.last_accessed_at).getTime(); - const daysSince = (now - lastAccessed) / (1000 * 60 * 60 * 24); - - const baseStability = node.stability_factor ?? SM2_MIN_STABILITY; - const sentimentIntensity = node.sentiment_intensity ?? 0; - const sentimentMultiplier = - SENTIMENT_MIN_BOOST + sentimentIntensity * (SENTIMENT_STABILITY_BOOST - SENTIMENT_MIN_BOOST); - const effectiveStability = baseStability * sentimentMultiplier; - - // Ebbinghaus forgetting curve: R = e^(-t/S) - const newRetention = Math.max(0.1, node.retention_strength * Math.exp(-daysSince / effectiveStability)); - - const updateStmt = this.db.prepare(` - UPDATE knowledge_nodes SET retention_strength = ? WHERE id = ? - `); - updateStmt.run(newRetention, id); - - return newRetention; - } catch (error) { - if (error instanceof NotFoundError) throw error; - throw new DatabaseError(`Failed to apply decay to node: ${id}`, error); - } - }); - } - - async applyDecayAll(): Promise { - return this.lock.withWriteLock(async () => { - try { - const now = Date.now(); - - // Use IMMEDIATE transaction for consistency - const transaction = this.db.transaction(() => { - const nodes = this.db - .prepare( - ` - SELECT id, last_accessed_at, retention_strength, stability_factor, sentiment_intensity - FROM knowledge_nodes - ` - ) - .all() as { - id: string; - last_accessed_at: string; - retention_strength: number; - stability_factor: number | null; - sentiment_intensity: number | null; - }[]; - - let updated = 0; - const updateStmt = this.db.prepare(` - UPDATE knowledge_nodes SET retention_strength = ? WHERE id = ? - `); - - for (const node of nodes) { - const lastAccessed = new Date(node.last_accessed_at).getTime(); - const daysSince = (now - lastAccessed) / (1000 * 60 * 60 * 24); - - const baseStability = node.stability_factor ?? SM2_MIN_STABILITY; - const sentimentIntensity = node.sentiment_intensity ?? 0; - const sentimentMultiplier = - SENTIMENT_MIN_BOOST + - sentimentIntensity * (SENTIMENT_STABILITY_BOOST - SENTIMENT_MIN_BOOST); - const effectiveStability = baseStability * sentimentMultiplier; - - const newRetention = Math.max( - 0.1, - node.retention_strength * Math.exp(-daysSince / effectiveStability) - ); - - if (Math.abs(newRetention - node.retention_strength) > 0.01) { - updateStmt.run(newRetention, node.id); - updated++; - } - } - - return updated; - }); - - return transaction.immediate(); - } catch (error) { - throw new DatabaseError('Failed to apply decay to all nodes', error); - } - }); - } - - // -------------------------------------------------------------------------- - // PRIVATE HELPERS - // -------------------------------------------------------------------------- - - /** - * Convert a database row to a KnowledgeNode entity - */ - private rowToEntity(row: Record): KnowledgeNode { - // Parse git context separately with proper null handling - let gitContext: GitContext | undefined; - if (row['git_context']) { - const parsed = safeJsonParse(row['git_context'] as string, null); - if (parsed !== null) { - gitContext = parsed; - } - } - - return { - id: row['id'] as string, - content: row['content'] as string, - summary: row['summary'] as string | undefined, - createdAt: new Date(row['created_at'] as string), - updatedAt: new Date(row['updated_at'] as string), - lastAccessedAt: new Date(row['last_accessed_at'] as string), - accessCount: row['access_count'] as number, - retentionStrength: row['retention_strength'] as number, - stabilityFactor: (row['stability_factor'] as number) ?? SM2_MIN_STABILITY, - sentimentIntensity: (row['sentiment_intensity'] as number) ?? 0, - // Dual-strength memory model fields - storageStrength: (row['storage_strength'] as number) ?? 1, - retrievalStrength: (row['retrieval_strength'] as number) ?? 1, - nextReviewDate: row['next_review_date'] - ? new Date(row['next_review_date'] as string) - : undefined, - reviewCount: row['review_count'] as number, - sourceType: row['source_type'] as KnowledgeNode['sourceType'], - sourcePlatform: row['source_platform'] as KnowledgeNode['sourcePlatform'], - sourceId: row['source_id'] as string | undefined, - sourceUrl: row['source_url'] as string | undefined, - sourceChain: safeJsonParse(row['source_chain'] as string, []), - gitContext, - confidence: row['confidence'] as number, - isContradicted: Boolean(row['is_contradicted']), - contradictionIds: safeJsonParse(row['contradiction_ids'] as string, []), - people: safeJsonParse(row['people'] as string, []), - concepts: safeJsonParse(row['concepts'] as string, []), - events: safeJsonParse(row['events'] as string, []), - tags: safeJsonParse(row['tags'] as string, []), - }; - } -} diff --git a/packages/core/src/repositories/PersonRepository.ts b/packages/core/src/repositories/PersonRepository.ts deleted file mode 100644 index 4a04aa5..0000000 --- a/packages/core/src/repositories/PersonRepository.ts +++ /dev/null @@ -1,864 +0,0 @@ -import Database from 'better-sqlite3'; -import { nanoid } from 'nanoid'; -import type { PersonNode } from '../core/types.js'; - -// ============================================================================ -// CONSTANTS -// ============================================================================ - -const DEFAULT_LIMIT = 50; -const MAX_LIMIT = 500; -const MAX_NAME_LENGTH = 500; -const MAX_CONTENT_LENGTH = 1_000_000; -const MAX_ARRAY_COUNT = 100; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface PaginationOptions { - limit?: number; - offset?: number; -} - -export interface PaginatedResult { - items: T[]; - total: number; - limit: number; - offset: number; - hasMore: boolean; -} - -export interface PersonNodeInput { - name: string; - aliases?: string[]; - howWeMet?: string; - relationshipType?: string; - organization?: string; - role?: string; - location?: string; - email?: string; - phone?: string; - socialLinks?: Record; - preferredChannel?: string; - sharedTopics?: string[]; - sharedProjects?: string[]; - notes?: string; - relationshipHealth?: number; - lastContactAt?: Date; - contactFrequency?: number; -} - -// ============================================================================ -// ERROR TYPE -// ============================================================================ - -export class PersonRepositoryError extends Error { - constructor( - message: string, - public readonly code: string, - cause?: unknown - ) { - super(sanitizeErrorMessage(message)); - this.name = 'PersonRepositoryError'; - if (process.env['NODE_ENV'] === 'development' && cause) { - this.cause = cause; - } - } -} - -// ============================================================================ -// HELPER FUNCTIONS -// ============================================================================ - -/** - * Sanitize error message to prevent sensitive data leakage - */ -function sanitizeErrorMessage(message: string): string { - let sanitized = message.replace(/\/[^\s]+/g, '[PATH]'); - sanitized = sanitized.replace(/SELECT|INSERT|UPDATE|DELETE|DROP|CREATE/gi, '[SQL]'); - sanitized = sanitized.replace(/\b(password|secret|key|token|auth)\s*[=:]\s*\S+/gi, '[REDACTED]'); - return sanitized; -} - -/** - * Safe JSON parse with fallback - never throws - */ -function safeJsonParse(value: string | null | undefined, fallback: T): T { - if (!value) return fallback; - try { - const parsed = JSON.parse(value); - if (typeof parsed !== typeof fallback) { - return fallback; - } - return parsed as T; - } catch { - return fallback; - } -} - -/** - * Validate string length for inputs - */ -function validateStringLength(value: string | undefined, maxLength: number, fieldName: string): void { - if (value && value.length > maxLength) { - throw new PersonRepositoryError( - `${fieldName} exceeds maximum length of ${maxLength} characters`, - 'INPUT_TOO_LONG' - ); - } -} - -/** - * Validate array length for inputs - */ -function validateArrayLength(arr: T[] | undefined, maxLength: number, fieldName: string): void { - if (arr && arr.length > maxLength) { - throw new PersonRepositoryError( - `${fieldName} exceeds maximum count of ${maxLength} items`, - 'INPUT_TOO_MANY_ITEMS' - ); - } -} - -// ============================================================================ -// READ-WRITE LOCK -// ============================================================================ - -/** - * A simple read-write lock for concurrent access control. - * Allows multiple readers or a single writer, but not both. - */ -export class RWLock { - private readers = 0; - private writer = false; - private readQueue: (() => void)[] = []; - private writeQueue: (() => void)[] = []; - - /** - * Acquire a read lock. Multiple readers can hold the lock simultaneously. - */ - async acquireRead(): Promise { - return new Promise((resolve) => { - if (!this.writer && this.writeQueue.length === 0) { - this.readers++; - resolve(); - } else { - this.readQueue.push(() => { - this.readers++; - resolve(); - }); - } - }); - } - - /** - * Release a read lock. - */ - releaseRead(): void { - this.readers--; - if (this.readers === 0) { - this.processWriteQueue(); - } - } - - /** - * Acquire a write lock. Only one writer can hold the lock at a time. - */ - async acquireWrite(): Promise { - return new Promise((resolve) => { - if (!this.writer && this.readers === 0) { - this.writer = true; - resolve(); - } else { - this.writeQueue.push(() => { - this.writer = true; - resolve(); - }); - } - }); - } - - /** - * Release a write lock. - */ - releaseWrite(): void { - this.writer = false; - // Process read queue first to prevent writer starvation - this.processReadQueue(); - if (this.readers === 0) { - this.processWriteQueue(); - } - } - - private processReadQueue(): void { - while (this.readQueue.length > 0 && !this.writer) { - const next = this.readQueue.shift(); - if (next) next(); - } - } - - private processWriteQueue(): void { - if (this.writeQueue.length > 0 && this.readers === 0 && !this.writer) { - const next = this.writeQueue.shift(); - if (next) next(); - } - } - - /** - * Execute a function with a read lock. - */ - async withRead(fn: () => T | Promise): Promise { - await this.acquireRead(); - try { - return await fn(); - } finally { - this.releaseRead(); - } - } - - /** - * Execute a function with a write lock. - */ - async withWrite(fn: () => T | Promise): Promise { - await this.acquireWrite(); - try { - return await fn(); - } finally { - this.releaseWrite(); - } - } -} - -// ============================================================================ -// INTERFACE -// ============================================================================ - -export interface IPersonRepository { - findById(id: string): Promise; - findByName(name: string): Promise; - searchByName(query: string, options?: PaginationOptions): Promise>; - create(input: PersonNodeInput): Promise; - update(id: string, updates: Partial): Promise; - delete(id: string): Promise; - getPeopleToReconnect(daysSinceContact: number, options?: PaginationOptions): Promise>; - recordContact(id: string): Promise; - findByOrganization(org: string, options?: PaginationOptions): Promise>; - findBySharedTopic(topic: string, options?: PaginationOptions): Promise>; - getAll(options?: PaginationOptions): Promise>; -} - -// ============================================================================ -// IMPLEMENTATION -// ============================================================================ - -export class PersonRepository implements IPersonRepository { - private readonly lock = new RWLock(); - - constructor(private readonly db: Database.Database) {} - - /** - * Convert a database row to a PersonNode entity. - */ - private rowToEntity(row: Record): PersonNode { - return { - id: row['id'] as string, - name: row['name'] as string, - aliases: safeJsonParse(row['aliases'] as string, []), - howWeMet: row['how_we_met'] as string | undefined, - relationshipType: row['relationship_type'] as string | undefined, - organization: row['organization'] as string | undefined, - role: row['role'] as string | undefined, - location: row['location'] as string | undefined, - email: row['email'] as string | undefined, - phone: row['phone'] as string | undefined, - socialLinks: safeJsonParse>(row['social_links'] as string, {}), - lastContactAt: row['last_contact_at'] ? new Date(row['last_contact_at'] as string) : undefined, - contactFrequency: row['contact_frequency'] as number, - preferredChannel: row['preferred_channel'] as string | undefined, - sharedTopics: safeJsonParse(row['shared_topics'] as string, []), - sharedProjects: safeJsonParse(row['shared_projects'] as string, []), - notes: row['notes'] as string | undefined, - relationshipHealth: row['relationship_health'] as number, - createdAt: new Date(row['created_at'] as string), - updatedAt: new Date(row['updated_at'] as string), - }; - } - - /** - * Validate input for creating or updating a person. - */ - private validateInput(input: PersonNodeInput | Partial, isCreate: boolean): void { - if (isCreate && !input.name) { - throw new PersonRepositoryError('Name is required', 'NAME_REQUIRED'); - } - - validateStringLength(input.name, MAX_NAME_LENGTH, 'Name'); - validateStringLength(input.notes, MAX_CONTENT_LENGTH, 'Notes'); - validateStringLength(input.howWeMet, MAX_CONTENT_LENGTH, 'How we met'); - validateArrayLength(input.aliases, MAX_ARRAY_COUNT, 'Aliases'); - validateArrayLength(input.sharedTopics, MAX_ARRAY_COUNT, 'Shared topics'); - validateArrayLength(input.sharedProjects, MAX_ARRAY_COUNT, 'Shared projects'); - } - - /** - * Find a person by their unique ID. - */ - async findById(id: string): Promise { - return this.lock.withRead(() => { - try { - const stmt = this.db.prepare('SELECT * FROM people WHERE id = ?'); - const row = stmt.get(id) as Record | undefined; - if (!row) return null; - return this.rowToEntity(row); - } catch (error) { - throw new PersonRepositoryError( - `Failed to find person: ${id}`, - 'FIND_BY_ID_FAILED', - error - ); - } - }); - } - - /** - * Find a person by their name or alias. - */ - async findByName(name: string): Promise { - return this.lock.withRead(() => { - try { - validateStringLength(name, MAX_NAME_LENGTH, 'Name'); - - // Escape special LIKE characters to prevent injection - const escapedName = name - .replace(/\\/g, '\\\\') - .replace(/%/g, '\\%') - .replace(/_/g, '\\_') - .replace(/"/g, '\\"'); - - const stmt = this.db.prepare(` - SELECT * FROM people - WHERE name = ? OR aliases LIKE ? ESCAPE '\\' - `); - const row = stmt.get(name, `%"${escapedName}"%`) as Record | undefined; - if (!row) return null; - return this.rowToEntity(row); - } catch (error) { - if (error instanceof PersonRepositoryError) throw error; - throw new PersonRepositoryError( - 'Failed to find person by name', - 'FIND_BY_NAME_FAILED', - error - ); - } - }); - } - - /** - * Search for people by name (partial match). - */ - async searchByName(query: string, options: PaginationOptions = {}): Promise> { - return this.lock.withRead(() => { - try { - validateStringLength(query, MAX_NAME_LENGTH, 'Search query'); - - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(Math.max(1, limit), MAX_LIMIT); - const safeOffset = Math.max(0, offset); - - // Escape special LIKE characters - const escapedQuery = query - .replace(/\\/g, '\\\\') - .replace(/%/g, '\\%') - .replace(/_/g, '\\_'); - - const searchPattern = `%${escapedQuery}%`; - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM people - WHERE name LIKE ? ESCAPE '\\' OR aliases LIKE ? ESCAPE '\\' - `); - const countResult = countStmt.get(searchPattern, searchPattern) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM people - WHERE name LIKE ? ESCAPE '\\' OR aliases LIKE ? ESCAPE '\\' - ORDER BY name - LIMIT ? OFFSET ? - `); - const rows = stmt.all(searchPattern, searchPattern, safeLimit, safeOffset) as Record[]; - const items = rows.map(row => this.rowToEntity(row)); - - return { - items, - total, - limit: safeLimit, - offset: safeOffset, - hasMore: safeOffset + items.length < total, - }; - } catch (error) { - if (error instanceof PersonRepositoryError) throw error; - throw new PersonRepositoryError( - 'Search by name failed', - 'SEARCH_BY_NAME_FAILED', - error - ); - } - }); - } - - /** - * Create a new person. - */ - async create(input: PersonNodeInput): Promise { - return this.lock.withWrite(() => { - try { - this.validateInput(input, true); - - // Validate relationship health is within bounds - const relationshipHealth = Math.max(0, Math.min(1, input.relationshipHealth ?? 0.5)); - - const id = nanoid(); - const now = new Date().toISOString(); - - const stmt = this.db.prepare(` - INSERT INTO people ( - id, name, aliases, - how_we_met, relationship_type, organization, role, location, - email, phone, social_links, - last_contact_at, contact_frequency, preferred_channel, - shared_topics, shared_projects, - notes, relationship_health, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - id, - input.name, - JSON.stringify(input.aliases || []), - input.howWeMet || null, - input.relationshipType || null, - input.organization || null, - input.role || null, - input.location || null, - input.email || null, - input.phone || null, - JSON.stringify(input.socialLinks || {}), - input.lastContactAt?.toISOString() || null, - input.contactFrequency || 0, - input.preferredChannel || null, - JSON.stringify(input.sharedTopics || []), - JSON.stringify(input.sharedProjects || []), - input.notes || null, - relationshipHealth, - now, - now - ); - - return { - id, - name: input.name, - aliases: input.aliases || [], - howWeMet: input.howWeMet, - relationshipType: input.relationshipType, - organization: input.organization, - role: input.role, - location: input.location, - email: input.email, - phone: input.phone, - socialLinks: input.socialLinks || {}, - lastContactAt: input.lastContactAt, - contactFrequency: input.contactFrequency || 0, - preferredChannel: input.preferredChannel, - sharedTopics: input.sharedTopics || [], - sharedProjects: input.sharedProjects || [], - notes: input.notes, - relationshipHealth, - createdAt: new Date(now), - updatedAt: new Date(now), - }; - } catch (error) { - if (error instanceof PersonRepositoryError) throw error; - throw new PersonRepositoryError( - 'Failed to create person', - 'CREATE_FAILED', - error - ); - } - }); - } - - /** - * Update an existing person. - */ - async update(id: string, updates: Partial): Promise { - return this.lock.withWrite(() => { - try { - this.validateInput(updates, false); - - // First check if the person exists - const existingStmt = this.db.prepare('SELECT * FROM people WHERE id = ?'); - const existing = existingStmt.get(id) as Record | undefined; - if (!existing) return null; - - const now = new Date().toISOString(); - - // Build update statement dynamically based on provided fields - const setClauses: string[] = ['updated_at = ?']; - const values: unknown[] = [now]; - - if (updates.name !== undefined) { - setClauses.push('name = ?'); - values.push(updates.name); - } - if (updates.aliases !== undefined) { - setClauses.push('aliases = ?'); - values.push(JSON.stringify(updates.aliases)); - } - if (updates.howWeMet !== undefined) { - setClauses.push('how_we_met = ?'); - values.push(updates.howWeMet || null); - } - if (updates.relationshipType !== undefined) { - setClauses.push('relationship_type = ?'); - values.push(updates.relationshipType || null); - } - if (updates.organization !== undefined) { - setClauses.push('organization = ?'); - values.push(updates.organization || null); - } - if (updates.role !== undefined) { - setClauses.push('role = ?'); - values.push(updates.role || null); - } - if (updates.location !== undefined) { - setClauses.push('location = ?'); - values.push(updates.location || null); - } - if (updates.email !== undefined) { - setClauses.push('email = ?'); - values.push(updates.email || null); - } - if (updates.phone !== undefined) { - setClauses.push('phone = ?'); - values.push(updates.phone || null); - } - if (updates.socialLinks !== undefined) { - setClauses.push('social_links = ?'); - values.push(JSON.stringify(updates.socialLinks)); - } - if (updates.lastContactAt !== undefined) { - setClauses.push('last_contact_at = ?'); - values.push(updates.lastContactAt?.toISOString() || null); - } - if (updates.contactFrequency !== undefined) { - setClauses.push('contact_frequency = ?'); - values.push(updates.contactFrequency); - } - if (updates.preferredChannel !== undefined) { - setClauses.push('preferred_channel = ?'); - values.push(updates.preferredChannel || null); - } - if (updates.sharedTopics !== undefined) { - setClauses.push('shared_topics = ?'); - values.push(JSON.stringify(updates.sharedTopics)); - } - if (updates.sharedProjects !== undefined) { - setClauses.push('shared_projects = ?'); - values.push(JSON.stringify(updates.sharedProjects)); - } - if (updates.notes !== undefined) { - setClauses.push('notes = ?'); - values.push(updates.notes || null); - } - if (updates.relationshipHealth !== undefined) { - const health = Math.max(0, Math.min(1, updates.relationshipHealth)); - setClauses.push('relationship_health = ?'); - values.push(health); - } - - values.push(id); - - const stmt = this.db.prepare(` - UPDATE people - SET ${setClauses.join(', ')} - WHERE id = ? - `); - stmt.run(...values); - - // Fetch and return the updated person - const updatedRow = existingStmt.get(id) as Record; - return this.rowToEntity(updatedRow); - } catch (error) { - if (error instanceof PersonRepositoryError) throw error; - throw new PersonRepositoryError( - `Failed to update person: ${id}`, - 'UPDATE_FAILED', - error - ); - } - }); - } - - /** - * Delete a person by ID. - */ - async delete(id: string): Promise { - return this.lock.withWrite(() => { - try { - const stmt = this.db.prepare('DELETE FROM people WHERE id = ?'); - const result = stmt.run(id); - return result.changes > 0; - } catch (error) { - throw new PersonRepositoryError( - `Failed to delete person: ${id}`, - 'DELETE_FAILED', - error - ); - } - }); - } - - /** - * Get people who haven't been contacted recently. - */ - async getPeopleToReconnect( - daysSinceContact: number = 30, - options: PaginationOptions = {} - ): Promise> { - return this.lock.withRead(() => { - try { - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(Math.max(1, limit), MAX_LIMIT); - const safeOffset = Math.max(0, offset); - - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - daysSinceContact); - const cutoffStr = cutoffDate.toISOString(); - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM people - WHERE last_contact_at IS NOT NULL AND last_contact_at < ? - `); - const countResult = countStmt.get(cutoffStr) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM people - WHERE last_contact_at IS NOT NULL - AND last_contact_at < ? - ORDER BY last_contact_at ASC - LIMIT ? OFFSET ? - `); - const rows = stmt.all(cutoffStr, safeLimit, safeOffset) as Record[]; - const items = rows.map(row => this.rowToEntity(row)); - - return { - items, - total, - limit: safeLimit, - offset: safeOffset, - hasMore: safeOffset + items.length < total, - }; - } catch (error) { - throw new PersonRepositoryError( - 'Failed to get people to reconnect', - 'GET_RECONNECT_FAILED', - error - ); - } - }); - } - - /** - * Record a contact with a person (updates last_contact_at). - */ - async recordContact(id: string): Promise { - return this.lock.withWrite(() => { - try { - const stmt = this.db.prepare(` - UPDATE people - SET last_contact_at = ?, updated_at = ? - WHERE id = ? - `); - const now = new Date().toISOString(); - const result = stmt.run(now, now, id); - - if (result.changes === 0) { - throw new PersonRepositoryError( - `Person not found: ${id}`, - 'PERSON_NOT_FOUND' - ); - } - } catch (error) { - if (error instanceof PersonRepositoryError) throw error; - throw new PersonRepositoryError( - `Failed to record contact: ${id}`, - 'RECORD_CONTACT_FAILED', - error - ); - } - }); - } - - /** - * Find people by organization. - */ - async findByOrganization( - org: string, - options: PaginationOptions = {} - ): Promise> { - return this.lock.withRead(() => { - try { - validateStringLength(org, MAX_NAME_LENGTH, 'Organization'); - - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(Math.max(1, limit), MAX_LIMIT); - const safeOffset = Math.max(0, offset); - - // Escape special LIKE characters - const escapedOrg = org - .replace(/\\/g, '\\\\') - .replace(/%/g, '\\%') - .replace(/_/g, '\\_'); - - const searchPattern = `%${escapedOrg}%`; - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM people - WHERE organization LIKE ? ESCAPE '\\' - `); - const countResult = countStmt.get(searchPattern) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM people - WHERE organization LIKE ? ESCAPE '\\' - ORDER BY name - LIMIT ? OFFSET ? - `); - const rows = stmt.all(searchPattern, safeLimit, safeOffset) as Record[]; - const items = rows.map(row => this.rowToEntity(row)); - - return { - items, - total, - limit: safeLimit, - offset: safeOffset, - hasMore: safeOffset + items.length < total, - }; - } catch (error) { - if (error instanceof PersonRepositoryError) throw error; - throw new PersonRepositoryError( - 'Failed to find people by organization', - 'FIND_BY_ORG_FAILED', - error - ); - } - }); - } - - /** - * Find people by shared topic. - */ - async findBySharedTopic( - topic: string, - options: PaginationOptions = {} - ): Promise> { - return this.lock.withRead(() => { - try { - validateStringLength(topic, MAX_NAME_LENGTH, 'Topic'); - - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(Math.max(1, limit), MAX_LIMIT); - const safeOffset = Math.max(0, offset); - - // Escape special LIKE characters and quotes for JSON search - const escapedTopic = topic - .replace(/\\/g, '\\\\') - .replace(/%/g, '\\%') - .replace(/_/g, '\\_') - .replace(/"/g, '\\"'); - - const searchPattern = `%"${escapedTopic}"%`; - - // Get total count - const countStmt = this.db.prepare(` - SELECT COUNT(*) as total FROM people - WHERE shared_topics LIKE ? ESCAPE '\\' - `); - const countResult = countStmt.get(searchPattern) as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare(` - SELECT * FROM people - WHERE shared_topics LIKE ? ESCAPE '\\' - ORDER BY name - LIMIT ? OFFSET ? - `); - const rows = stmt.all(searchPattern, safeLimit, safeOffset) as Record[]; - const items = rows.map(row => this.rowToEntity(row)); - - return { - items, - total, - limit: safeLimit, - offset: safeOffset, - hasMore: safeOffset + items.length < total, - }; - } catch (error) { - if (error instanceof PersonRepositoryError) throw error; - throw new PersonRepositoryError( - 'Failed to find people by shared topic', - 'FIND_BY_TOPIC_FAILED', - error - ); - } - }); - } - - /** - * Get all people with pagination. - */ - async getAll(options: PaginationOptions = {}): Promise> { - return this.lock.withRead(() => { - try { - const { limit = DEFAULT_LIMIT, offset = 0 } = options; - const safeLimit = Math.min(Math.max(1, limit), MAX_LIMIT); - const safeOffset = Math.max(0, offset); - - // Get total count - const countResult = this.db.prepare('SELECT COUNT(*) as total FROM people').get() as { total: number }; - const total = countResult.total; - - // Get paginated results - const stmt = this.db.prepare('SELECT * FROM people ORDER BY name LIMIT ? OFFSET ?'); - const rows = stmt.all(safeLimit, safeOffset) as Record[]; - const items = rows.map(row => this.rowToEntity(row)); - - return { - items, - total, - limit: safeLimit, - offset: safeOffset, - hasMore: safeOffset + items.length < total, - }; - } catch (error) { - throw new PersonRepositoryError( - 'Failed to get all people', - 'GET_ALL_FAILED', - error - ); - } - }); - } -} diff --git a/packages/core/src/repositories/index.ts b/packages/core/src/repositories/index.ts deleted file mode 100644 index b7c6a8b..0000000 --- a/packages/core/src/repositories/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Re-export from NodeRepository (primary source for common types) -export { - NodeRepository, - type INodeRepository, - type PaginationOptions, - type PaginatedResult, - type GitContext, -} from './NodeRepository.js'; - -// Re-export from PersonRepository (exclude duplicate types) -export { - PersonRepository, - type IPersonRepository, - type PersonNodeInput, - PersonRepositoryError, -} from './PersonRepository.js'; - -// Re-export from EdgeRepository (exclude duplicate types) -export { - EdgeRepository, - type IEdgeRepository, - type GraphEdgeInput, - type EdgeType, - type TransitivePath, - EdgeRepositoryError, -} from './EdgeRepository.js'; diff --git a/packages/core/src/services/CacheService.ts b/packages/core/src/services/CacheService.ts deleted file mode 100644 index 3b92a46..0000000 --- a/packages/core/src/services/CacheService.ts +++ /dev/null @@ -1,603 +0,0 @@ -import type { KnowledgeNode, PersonNode } from '../core/types.js'; -import type { PaginatedResult } from '../repositories/PersonRepository.js'; - -// ============================================================================ -// TYPES -// ============================================================================ - -/** - * Represents a single entry in the cache with metadata for TTL and LRU eviction. - */ -export interface CacheEntry { - /** The cached value */ - value: T; - /** Unix timestamp (ms) when this entry expires */ - expiresAt: number; - /** Number of times this entry has been accessed */ - accessCount: number; - /** Unix timestamp (ms) of the last access */ - lastAccessed: number; - /** Estimated size in bytes (optional, for memory-based eviction) */ - size?: number; -} - -/** - * Configuration options for the cache service. - */ -export interface CacheOptions { - /** Maximum number of entries in the cache */ - maxSize: number; - /** Maximum memory usage in bytes (optional) */ - maxMemory?: number; - /** Default TTL in milliseconds */ - defaultTTL: number; - /** Interval in milliseconds for automatic cleanup of expired entries */ - cleanupInterval: number; -} - -/** - * Statistics about cache performance and state. - */ -export interface CacheStats { - /** Current number of entries in the cache */ - size: number; - /** Hit rate as a ratio (0-1) */ - hitRate: number; - /** Estimated memory usage in bytes */ - memoryUsage: number; -} - -// ============================================================================ -// CONSTANTS -// ============================================================================ - -const DEFAULT_OPTIONS: CacheOptions = { - maxSize: 10000, - defaultTTL: 5 * 60 * 1000, // 5 minutes - cleanupInterval: 60 * 1000, // 1 minute -}; - -// ============================================================================ -// CACHE SERVICE -// ============================================================================ - -/** - * A generic in-memory cache service with TTL support and LRU eviction. - * - * Features: - * - Time-based expiration (TTL) - * - LRU eviction when max size is reached - * - Memory-based eviction (optional) - * - Automatic cleanup of expired entries - * - Pattern-based invalidation - * - Cache-aside pattern support (getOrCompute) - * - Hit rate tracking - * - * @template T The type of values stored in the cache - */ -export class CacheService { - private cache: Map> = new Map(); - private options: CacheOptions; - private cleanupTimer: ReturnType | null = null; - private hits = 0; - private misses = 0; - private totalMemory = 0; - - constructor(options?: Partial) { - this.options = { ...DEFAULT_OPTIONS, ...options }; - this.startCleanupTimer(); - } - - // -------------------------------------------------------------------------- - // PUBLIC METHODS - // -------------------------------------------------------------------------- - - /** - * Get a value from cache. - * Updates access metadata if the entry exists and is not expired. - * - * @param key The cache key - * @returns The cached value, or undefined if not found or expired - */ - get(key: string): T | undefined { - const entry = this.cache.get(key); - - if (!entry) { - this.misses++; - return undefined; - } - - // Check if expired - if (Date.now() > entry.expiresAt) { - this.deleteEntry(key, entry); - this.misses++; - return undefined; - } - - // Update access metadata - entry.accessCount++; - entry.lastAccessed = Date.now(); - this.hits++; - - return entry.value; - } - - /** - * Set a value in cache. - * Performs LRU eviction if the cache is at capacity. - * - * @param key The cache key - * @param value The value to cache - * @param ttl Optional TTL in milliseconds (defaults to configured defaultTTL) - */ - set(key: string, value: T, ttl?: number): void { - const now = Date.now(); - const effectiveTTL = ttl ?? this.options.defaultTTL; - const size = this.estimateSize(value); - - // If key already exists, remove old entry's size from total - const existingEntry = this.cache.get(key); - if (existingEntry) { - this.totalMemory -= existingEntry.size ?? 0; - } - - // Evict entries if needed (before adding new entry) - this.evictIfNeeded(size); - - const entry: CacheEntry = { - value, - expiresAt: now + effectiveTTL, - accessCount: 0, - lastAccessed: now, - size, - }; - - this.cache.set(key, entry); - this.totalMemory += size; - } - - /** - * Delete a key from cache. - * - * @param key The cache key to delete - * @returns true if the key was deleted, false if it didn't exist - */ - delete(key: string): boolean { - const entry = this.cache.get(key); - if (entry) { - this.deleteEntry(key, entry); - return true; - } - return false; - } - - /** - * Check if a key exists in cache and is not expired. - * - * @param key The cache key - * @returns true if the key exists and is not expired - */ - has(key: string): boolean { - const entry = this.cache.get(key); - if (!entry) return false; - - if (Date.now() > entry.expiresAt) { - this.deleteEntry(key, entry); - return false; - } - - return true; - } - - /** - * Invalidate all keys matching a pattern. - * - * @param pattern A RegExp pattern to match keys against - * @returns The number of keys invalidated - */ - invalidatePattern(pattern: RegExp): number { - let count = 0; - const keysToDelete: string[] = []; - - for (const key of this.cache.keys()) { - if (pattern.test(key)) { - keysToDelete.push(key); - } - } - - for (const key of keysToDelete) { - const entry = this.cache.get(key); - if (entry) { - this.deleteEntry(key, entry); - count++; - } - } - - return count; - } - - /** - * Clear all entries from the cache. - */ - clear(): void { - this.cache.clear(); - this.totalMemory = 0; - this.hits = 0; - this.misses = 0; - } - - /** - * Get or compute a value (cache-aside pattern). - * If the key exists and is not expired, returns the cached value. - * Otherwise, computes the value using the provided function, caches it, and returns it. - * - * @param key The cache key - * @param compute A function that computes the value if not cached - * @param ttl Optional TTL in milliseconds - * @returns The cached or computed value - */ - async getOrCompute( - key: string, - compute: () => Promise, - ttl?: number - ): Promise { - // Try to get from cache first - const cached = this.get(key); - if (cached !== undefined) { - return cached; - } - - // Compute the value - const value = await compute(); - - // Cache and return - this.set(key, value, ttl); - return value; - } - - /** - * Get cache statistics. - * - * @returns Statistics about cache performance and state - */ - stats(): CacheStats { - const totalRequests = this.hits + this.misses; - return { - size: this.cache.size, - hitRate: totalRequests > 0 ? this.hits / totalRequests : 0, - memoryUsage: this.totalMemory, - }; - } - - /** - * Stop the cleanup timer and release resources. - * Call this when the cache is no longer needed to prevent memory leaks. - */ - destroy(): void { - if (this.cleanupTimer) { - clearInterval(this.cleanupTimer); - this.cleanupTimer = null; - } - this.clear(); - } - - // -------------------------------------------------------------------------- - // PRIVATE METHODS - // -------------------------------------------------------------------------- - - /** - * Start the automatic cleanup timer. - */ - private startCleanupTimer(): void { - if (this.options.cleanupInterval > 0) { - this.cleanupTimer = setInterval(() => { - this.cleanup(); - }, this.options.cleanupInterval); - - // Don't prevent Node.js from exiting if this is the only timer - if (this.cleanupTimer.unref) { - this.cleanupTimer.unref(); - } - } - } - - /** - * Remove expired entries from the cache. - */ - private cleanup(): void { - const now = Date.now(); - const keysToDelete: string[] = []; - - for (const [key, entry] of this.cache.entries()) { - if (now > entry.expiresAt) { - keysToDelete.push(key); - } - } - - for (const key of keysToDelete) { - const entry = this.cache.get(key); - if (entry) { - this.deleteEntry(key, entry); - } - } - } - - /** - * Delete an entry and update memory tracking. - */ - private deleteEntry(key: string, entry: CacheEntry): void { - this.totalMemory -= entry.size ?? 0; - this.cache.delete(key); - } - - /** - * Evict entries if the cache is at capacity. - * Uses LRU eviction strategy based on lastAccessed timestamp. - * Also considers memory limits if configured. - */ - private evictIfNeeded(incomingSize: number): void { - // Evict for size limit - while (this.cache.size >= this.options.maxSize) { - this.evictLRU(); - } - - // Evict for memory limit if configured - if (this.options.maxMemory) { - while ( - this.totalMemory + incomingSize > this.options.maxMemory && - this.cache.size > 0 - ) { - this.evictLRU(); - } - } - } - - /** - * Evict the least recently used entry. - * Finds the entry with the oldest lastAccessed timestamp and removes it. - */ - private evictLRU(): void { - let oldestKey: string | null = null; - let oldestTime = Infinity; - - for (const [key, entry] of this.cache.entries()) { - if (entry.lastAccessed < oldestTime) { - oldestTime = entry.lastAccessed; - oldestKey = key; - } - } - - if (oldestKey !== null) { - const entry = this.cache.get(oldestKey); - if (entry) { - this.deleteEntry(oldestKey, entry); - } - } - } - - /** - * Estimate the memory size of a value in bytes. - * This is a rough approximation for memory tracking purposes. - */ - private estimateSize(value: T): number { - if (value === null || value === undefined) { - return 8; - } - - const type = typeof value; - - if (type === 'boolean') { - return 4; - } - - if (type === 'number') { - return 8; - } - - if (type === 'string') { - return (value as string).length * 2 + 40; // 2 bytes per char + overhead - } - - if (Array.isArray(value)) { - // For arrays, estimate based on length - // This is a rough approximation - return 40 + (value as unknown[]).length * 8; - } - - if (type === 'object') { - // For objects, use JSON serialization as a rough estimate - try { - const json = JSON.stringify(value); - return json.length * 2 + 40; - } catch { - return 1024; // Default size for non-serializable objects - } - } - - return 8; - } -} - -// ============================================================================ -// CACHE KEY HELPERS -// ============================================================================ - -/** - * Standard cache key patterns for Vestige MCP. - * These functions generate consistent cache keys for different entity types. - */ -export const CACHE_KEYS = { - /** Cache key for a knowledge node by ID */ - node: (id: string): string => `node:${id}`, - - /** Cache key for a person by ID */ - person: (id: string): string => `person:${id}`, - - /** Cache key for search results */ - search: (query: string, opts: string): string => `search:${query}:${opts}`, - - /** Cache key for embeddings by node ID */ - embedding: (nodeId: string): string => `embedding:${nodeId}`, - - /** Cache key for related nodes */ - related: (nodeId: string, depth: number): string => `related:${nodeId}:${depth}`, - - /** Cache key for person by name */ - personByName: (name: string): string => `person:name:${name.toLowerCase()}`, - - /** Cache key for daily brief by date */ - dailyBrief: (date: string): string => `daily-brief:${date}`, -}; - -/** - * Pattern matchers for cache invalidation. - */ -export const CACHE_PATTERNS = { - /** All node-related entries */ - allNodes: /^node:/, - - /** All person-related entries */ - allPeople: /^person:/, - - /** All search results */ - allSearches: /^search:/, - - /** All embeddings */ - allEmbeddings: /^embedding:/, - - /** All related node entries */ - allRelated: /^related:/, - - /** Entries for a specific node and its related data */ - nodeAndRelated: (nodeId: string): RegExp => - new RegExp(`^(node:${nodeId}|related:${nodeId}|embedding:${nodeId})`), - - /** Entries for a specific person and related data */ - personAndRelated: (personId: string): RegExp => - new RegExp(`^person:(${personId}|name:)`), -}; - -// ============================================================================ -// SPECIALIZED CACHE INSTANCES -// ============================================================================ - -/** - * Cache for KnowledgeNode entities. - * Longer TTL since nodes don't change frequently. - */ -export const nodeCache = new CacheService({ - maxSize: 5000, - defaultTTL: 10 * 60 * 1000, // 10 minutes - cleanupInterval: 2 * 60 * 1000, // 2 minutes -}); - -/** - * Cache for search results. - * Shorter TTL since search results can change with new data. - */ -export const searchCache = new CacheService>({ - maxSize: 1000, - defaultTTL: 60 * 1000, // 1 minute - cleanupInterval: 30 * 1000, // 30 seconds -}); - -/** - * Cache for embedding vectors. - * Longer TTL since embeddings don't change for existing content. - */ -export const embeddingCache = new CacheService({ - maxSize: 10000, - defaultTTL: 60 * 60 * 1000, // 1 hour - cleanupInterval: 5 * 60 * 1000, // 5 minutes -}); - -/** - * Cache for PersonNode entities. - */ -export const personCache = new CacheService({ - maxSize: 2000, - defaultTTL: 10 * 60 * 1000, // 10 minutes - cleanupInterval: 2 * 60 * 1000, // 2 minutes -}); - -/** - * Cache for related nodes queries. - */ -export const relatedCache = new CacheService({ - maxSize: 2000, - defaultTTL: 5 * 60 * 1000, // 5 minutes - cleanupInterval: 60 * 1000, // 1 minute -}); - -// ============================================================================ -// UTILITY FUNCTIONS -// ============================================================================ - -/** - * Invalidate all caches related to a specific node. - * Call this when a node is created, updated, or deleted. - * - * @param nodeId The ID of the node that changed - */ -export function invalidateNodeCaches(nodeId: string): void { - nodeCache.delete(CACHE_KEYS.node(nodeId)); - embeddingCache.delete(CACHE_KEYS.embedding(nodeId)); - - // Invalidate related entries and search results - relatedCache.invalidatePattern(new RegExp(`^related:${nodeId}`)); - searchCache.clear(); // Search results may be affected -} - -/** - * Invalidate all caches related to a specific person. - * Call this when a person is created, updated, or deleted. - * - * @param personId The ID of the person that changed - * @param name Optional name to also invalidate name-based lookups - */ -export function invalidatePersonCaches(personId: string, name?: string): void { - personCache.delete(CACHE_KEYS.person(personId)); - - if (name) { - personCache.delete(CACHE_KEYS.personByName(name)); - } - - // Search results may reference this person - searchCache.clear(); -} - -/** - * Clear all caches. Useful for testing or when major data changes occur. - */ -export function clearAllCaches(): void { - nodeCache.clear(); - searchCache.clear(); - embeddingCache.clear(); - personCache.clear(); - relatedCache.clear(); -} - -/** - * Get aggregated statistics from all caches. - */ -export function getAllCacheStats(): Record { - return { - node: nodeCache.stats(), - search: searchCache.stats(), - embedding: embeddingCache.stats(), - person: personCache.stats(), - related: relatedCache.stats(), - }; -} - -/** - * Destroy all cache instances and stop cleanup timers. - * Call this during application shutdown. - */ -export function destroyAllCaches(): void { - nodeCache.destroy(); - searchCache.destroy(); - embeddingCache.destroy(); - personCache.destroy(); - relatedCache.destroy(); -} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts deleted file mode 100644 index f790856..0000000 --- a/packages/core/src/utils/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Utility exports - */ - -export * from './mutex.js'; -export * from './json.js'; -export * from './logger.js'; diff --git a/packages/core/src/utils/json.ts b/packages/core/src/utils/json.ts deleted file mode 100644 index 619fae4..0000000 --- a/packages/core/src/utils/json.ts +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Safe JSON utilities for database operations - */ - -import { z } from 'zod'; -import { logger } from './logger.js'; - -/** - * Safely parse JSON with logging on failure - */ -export function safeJsonParse( - value: string | null | undefined, - fallback: T, - options?: { - logOnError?: boolean; - context?: string; - } -): T { - if (!value) return fallback; - - try { - const parsed = JSON.parse(value); - - // Type validation - if (typeof parsed !== typeof fallback) { - if (options?.logOnError !== false) { - logger.warn('JSON parse type mismatch', { - expected: typeof fallback, - got: typeof parsed, - context: options?.context, - }); - } - return fallback; - } - - // Array validation - if (Array.isArray(fallback) && !Array.isArray(parsed)) { - if (options?.logOnError !== false) { - logger.warn('JSON parse expected array', { - got: typeof parsed, - context: options?.context, - }); - } - return fallback; - } - - return parsed as T; - } catch (error) { - if (options?.logOnError !== false) { - logger.warn('JSON parse failed', { - error: (error as Error).message, - valuePreview: value.slice(0, 100), - context: options?.context, - }); - } - return fallback; - } -} - -/** - * Safely stringify JSON with circular reference handling - */ -export function safeJsonStringify( - value: unknown, - options?: { - replacer?: (key: string, value: unknown) => unknown; - space?: number; - maxDepth?: number; - } -): string { - const seen = new WeakSet(); - const maxDepth = options?.maxDepth ?? 10; - - function replacer( - this: unknown, - key: string, - value: unknown, - depth: number - ): unknown { - if (depth > maxDepth) { - return '[Max Depth Exceeded]'; - } - - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return '[Circular Reference]'; - } - seen.add(value); - } - - if (options?.replacer) { - return options.replacer(key, value); - } - - // Handle special types - if (value instanceof Error) { - return { - name: value.name, - message: value.message, - stack: value.stack, - }; - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (value instanceof Map) { - return Object.fromEntries(value); - } - - if (value instanceof Set) { - return Array.from(value); - } - - return value; - } - - try { - // Create a depth-tracking replacer - let currentDepth = 0; - return JSON.stringify( - value, - function (key, val) { - if (key === '') currentDepth = 0; - else currentDepth++; - return replacer.call(this, key, val, currentDepth); - }, - options?.space - ); - } catch (error) { - logger.error('JSON stringify failed', error as Error); - return '{}'; - } -} - -/** - * Parse JSON and validate against Zod schema - */ -export function parseJsonWithSchema( - value: string | null | undefined, - schema: T, - fallback: z.infer -): z.infer { - if (!value) return fallback; - - try { - const parsed = JSON.parse(value); - const result = schema.safeParse(parsed); - - if (result.success) { - return result.data; - } - - logger.warn('JSON schema validation failed', { - errors: result.error.errors, - }); - return fallback; - } catch (error) { - logger.warn('JSON parse failed for schema validation', { - error: (error as Error).message, - }); - return fallback; - } -} - -/** - * Calculate diff between two JSON objects - */ -export function jsonDiff( - before: Record, - after: Record -): { added: string[]; removed: string[]; changed: string[] } { - const added: string[] = []; - const removed: string[] = []; - const changed: string[] = []; - - // Check for added and changed - for (const key of Object.keys(after)) { - if (!(key in before)) { - added.push(key); - } else if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) { - changed.push(key); - } - } - - // Check for removed - for (const key of Object.keys(before)) { - if (!(key in after)) { - removed.push(key); - } - } - - return { added, removed, changed }; -} - -/** - * Deep merge JSON objects - */ -export function jsonMerge>( - target: T, - ...sources: Partial[] -): T { - const result = { ...target }; - - for (const source of sources) { - for (const key of Object.keys(source)) { - const targetVal = result[key as keyof T]; - const sourceVal = source[key as keyof T]; - - if ( - typeof targetVal === 'object' && - targetVal !== null && - typeof sourceVal === 'object' && - sourceVal !== null && - !Array.isArray(targetVal) && - !Array.isArray(sourceVal) - ) { - (result as Record)[key] = jsonMerge( - targetVal as Record, - sourceVal as Record - ); - } else if (sourceVal !== undefined) { - (result as Record)[key] = sourceVal; - } - } - } - - return result; -} diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts deleted file mode 100644 index 3dc8008..0000000 --- a/packages/core/src/utils/logger.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * Centralized logging system for Vestige MCP - * - * Provides structured JSON logging with: - * - Log levels (debug, info, warn, error) - * - Child loggers for subsystems - * - Request context tracking via AsyncLocalStorage - * - Performance logging utilities - */ - -import { AsyncLocalStorage } from 'async_hooks'; -import { nanoid } from 'nanoid'; - -// ============================================================================ -// Types -// ============================================================================ - -export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; - -export interface LogEntry { - timestamp: string; - level: LogLevel; - logger: string; - message: string; - context?: Record; - error?: { - name: string; - message: string; - stack?: string; - }; -} - -export interface Logger { - debug(message: string, context?: Record): void; - info(message: string, context?: Record): void; - warn(message: string, context?: Record): void; - error(message: string, error?: Error, context?: Record): void; - child(name: string): Logger; -} - -// ============================================================================ -// Request Context (AsyncLocalStorage) -// ============================================================================ - -interface RequestContext { - requestId: string; - startTime: number; -} - -export const requestContext = new AsyncLocalStorage(); - -/** - * Run a function within a request context for tracing - */ -export function withRequestContext(fn: () => T): T { - const ctx: RequestContext = { - requestId: nanoid(8), - startTime: Date.now(), - }; - return requestContext.run(ctx, fn); -} - -/** - * Run an async function within a request context for tracing - */ -export function withRequestContextAsync(fn: () => Promise): Promise { - const ctx: RequestContext = { - requestId: nanoid(8), - startTime: Date.now(), - }; - return requestContext.run(ctx, fn); -} - -/** - * Enrich context with request tracing information if available - */ -function enrichContext(context?: Record): Record { - const ctx = requestContext.getStore(); - if (ctx) { - return { - ...context, - requestId: ctx.requestId, - elapsed: Date.now() - ctx.startTime, - }; - } - return context || {}; -} - -// ============================================================================ -// Logger Implementation -// ============================================================================ - -const LOG_LEVELS: Record = { - debug: 0, - info: 1, - warn: 2, - error: 3, -}; - -/** - * Create a structured JSON logger - * - * @param name - Logger name (used as prefix for child loggers) - * @param minLevel - Minimum log level to output (default: 'info') - * @returns Logger instance - */ -export function createLogger(name: string, minLevel: LogLevel = 'info'): Logger { - const minLevelValue = LOG_LEVELS[minLevel]; - - function log( - level: LogLevel, - message: string, - context?: Record, - error?: Error - ): void { - if (LOG_LEVELS[level] < minLevelValue) return; - - const enrichedContext = enrichContext(context); - - const entry: LogEntry = { - timestamp: new Date().toISOString(), - level, - logger: name, - message, - }; - - // Only include context if it has properties - if (Object.keys(enrichedContext).length > 0) { - entry.context = enrichedContext; - } - - if (error) { - entry.error = { - name: error.name, - message: error.message, - ...(error.stack !== undefined && { stack: error.stack }), - }; - } - - const output = JSON.stringify(entry); - - if (level === 'error') { - console.error(output); - } else { - console.log(output); - } - } - - return { - debug: (message, context) => log('debug', message, context), - info: (message, context) => log('info', message, context), - warn: (message, context) => log('warn', message, context), - error: (message, error, context) => log('error', message, context, error), - child: (childName) => createLogger(`${name}:${childName}`, minLevel), - }; -} - -// ============================================================================ -// Global Logger Instances -// ============================================================================ - -// Get log level from environment -function getLogLevelFromEnv(): LogLevel { - const envLevel = process.env['VESTIGE_LOG_LEVEL']?.toLowerCase(); - if (envLevel && envLevel in LOG_LEVELS) { - return envLevel as LogLevel; - } - return 'info'; -} - -const LOG_LEVEL = getLogLevelFromEnv(); - -// Root logger -export const logger = createLogger('vestige', LOG_LEVEL); - -// Pre-configured child loggers for subsystems -export const dbLogger = logger.child('database'); -export const mcpLogger = logger.child('mcp'); -export const remLogger = logger.child('rem-cycle'); -export const embeddingLogger = logger.child('embeddings'); -export const cacheLogger = logger.child('cache'); -export const jobLogger = logger.child('jobs'); - -// ============================================================================ -// Performance Logging -// ============================================================================ - -/** - * Wrap a function to log its execution time - * - * @param logger - Logger instance to use - * @param operationName - Name of the operation for logging - * @param fn - Async function to wrap - * @returns Wrapped function that logs performance - * - * @example - * const wrappedFetch = logPerformance(dbLogger, 'fetchNodes', fetchNodes); - * const nodes = await wrappedFetch(query); - */ -export function logPerformance Promise>( - logger: Logger, - operationName: string, - fn: T -): T { - return (async (...args: Parameters) => { - const start = Date.now(); - try { - const result = await fn(...args); - logger.info(`${operationName} completed`, { - duration: Date.now() - start, - }); - return result; - } catch (error) { - logger.error(`${operationName} failed`, error as Error, { - duration: Date.now() - start, - }); - throw error; - } - }) as T; -} - -/** - * Log performance of a single async operation - * - * @param logger - Logger instance to use - * @param operationName - Name of the operation for logging - * @param fn - Async function to execute and measure - * @returns Result of the function - * - * @example - * const result = await timedOperation(dbLogger, 'query', async () => { - * return await db.query(sql); - * }); - */ -export async function timedOperation( - logger: Logger, - operationName: string, - fn: () => Promise -): Promise { - const start = Date.now(); - try { - const result = await fn(); - logger.info(`${operationName} completed`, { - duration: Date.now() - start, - }); - return result; - } catch (error) { - logger.error(`${operationName} failed`, error as Error, { - duration: Date.now() - start, - }); - throw error; - } -} diff --git a/packages/core/src/utils/mutex.ts b/packages/core/src/utils/mutex.ts deleted file mode 100644 index d3fb819..0000000 --- a/packages/core/src/utils/mutex.ts +++ /dev/null @@ -1,451 +0,0 @@ -/** - * Concurrency utilities for Vestige MCP - * - * Provides synchronization primitives for managing concurrent access - * to shared resources like database connections. - */ - -/** - * Error thrown when an operation times out - */ -export class TimeoutError extends Error { - constructor(message = "Operation timed out") { - super(message); - this.name = "TimeoutError"; - } -} - -/** - * Reader-Writer Lock for concurrent database access. - * Allows multiple concurrent readers OR one exclusive writer. - * - * This implementation uses writer preference with reader batching - * to prevent writer starvation while still allowing good read throughput. - */ -export class RWLock { - private readers = 0; - private writer = false; - private writerQueue: (() => void)[] = []; - private readerQueue: (() => void)[] = []; - - /** - * Execute a function with read lock (allows concurrent readers) - */ - async withReadLock(fn: () => Promise): Promise { - await this.acquireRead(); - try { - return await fn(); - } finally { - this.releaseRead(); - } - } - - /** - * Execute a function with write lock (exclusive access) - */ - async withWriteLock(fn: () => Promise): Promise { - await this.acquireWrite(); - try { - return await fn(); - } finally { - this.releaseWrite(); - } - } - - private acquireRead(): Promise { - return new Promise((resolve) => { - // If no writer and no writers waiting, grant immediately - if (!this.writer && this.writerQueue.length === 0) { - this.readers++; - resolve(); - } else { - // Queue the reader - this.readerQueue.push(() => { - this.readers++; - resolve(); - }); - } - }); - } - - private releaseRead(): void { - this.readers--; - - // If no more readers, wake up waiting writer - if (this.readers === 0 && this.writerQueue.length > 0) { - const nextWriter = this.writerQueue.shift(); - if (nextWriter) { - this.writer = true; - nextWriter(); - } - } - } - - private acquireWrite(): Promise { - return new Promise((resolve) => { - // If no readers and no writer, grant immediately - if (this.readers === 0 && !this.writer) { - this.writer = true; - resolve(); - } else { - // Queue the writer - this.writerQueue.push(resolve); - } - }); - } - - private releaseWrite(): void { - this.writer = false; - - // Prefer waking readers over writers to prevent starvation - // Wake all waiting readers as a batch - if (this.readerQueue.length > 0) { - const readers = this.readerQueue.splice(0, this.readerQueue.length); - for (const reader of readers) { - reader(); - } - } else if (this.writerQueue.length > 0) { - // No waiting readers, wake next writer - const nextWriter = this.writerQueue.shift(); - if (nextWriter) { - this.writer = true; - nextWriter(); - } - } - } - - /** - * Get current lock state (for debugging/monitoring) - */ - getState(): { readers: number; hasWriter: boolean; pendingReaders: number; pendingWriters: number } { - return { - readers: this.readers, - hasWriter: this.writer, - pendingReaders: this.readerQueue.length, - pendingWriters: this.writerQueue.length, - }; - } -} - -/** - * Simple mutex for exclusive access - */ -export class Mutex { - private locked = false; - private queue: (() => void)[] = []; - - /** - * Execute a function with exclusive lock - */ - async withLock(fn: () => Promise): Promise { - await this.acquire(); - try { - return await fn(); - } finally { - this.release(); - } - } - - private acquire(): Promise { - return new Promise((resolve) => { - if (!this.locked) { - this.locked = true; - resolve(); - } else { - this.queue.push(resolve); - } - }); - } - - private release(): void { - if (this.queue.length > 0) { - const next = this.queue.shift(); - if (next) { - next(); - } - } else { - this.locked = false; - } - } - - /** - * Check if the mutex is currently locked - */ - isLocked(): boolean { - return this.locked; - } - - /** - * Get the number of waiters in the queue - */ - getQueueLength(): number { - return this.queue.length; - } -} - -/** - * Semaphore for limiting concurrent operations - */ -export class Semaphore { - private permits: number; - private available: number; - private queue: (() => void)[] = []; - - constructor(permits: number) { - if (permits < 1) { - throw new Error("Semaphore must have at least 1 permit"); - } - this.permits = permits; - this.available = permits; - } - - /** - * Execute a function with a permit from the semaphore - */ - async withPermit(fn: () => Promise): Promise { - await this.acquire(); - try { - return await fn(); - } finally { - this.release(); - } - } - - /** - * Execute multiple functions concurrently, respecting the semaphore limit - */ - async map(items: T[], fn: (item: T) => Promise): Promise { - return Promise.all(items.map((item) => this.withPermit(() => fn(item)))); - } - - private acquire(): Promise { - return new Promise((resolve) => { - if (this.available > 0) { - this.available--; - resolve(); - } else { - this.queue.push(resolve); - } - }); - } - - private release(): void { - if (this.queue.length > 0) { - const next = this.queue.shift(); - if (next) { - next(); - } - } else { - this.available++; - } - } - - /** - * Get the number of available permits - */ - getAvailable(): number { - return this.available; - } - - /** - * Get the total number of permits - */ - getTotal(): number { - return this.permits; - } - - /** - * Get the number of waiters in the queue - */ - getQueueLength(): number { - return this.queue.length; - } -} - -/** - * Add timeout to any promise - * - * @param promise - The promise to wrap with a timeout - * @param ms - Timeout in milliseconds - * @param message - Optional custom error message - * @returns The result of the promise if it completes in time - * @throws TimeoutError if the timeout is exceeded - */ -export function withTimeout(promise: Promise, ms: number, message?: string): Promise { - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - reject(new TimeoutError(message ?? `Operation timed out after ${ms}ms`)); - }, ms); - - promise - .then((result) => { - clearTimeout(timeoutId); - resolve(result); - }) - .catch((error) => { - clearTimeout(timeoutId); - reject(error); - }); - }); -} - -/** - * Options for retry with exponential backoff - */ -export interface RetryOptions { - /** Maximum number of retry attempts (default: 3) */ - maxRetries?: number; - /** Initial delay in milliseconds (default: 100) */ - initialDelay?: number; - /** Maximum delay in milliseconds (default: 5000) */ - maxDelay?: number; - /** Backoff multiplier (default: 2) */ - backoffFactor?: number; - /** Optional function to determine if an error is retryable */ - isRetryable?: (error: unknown) => boolean; - /** Optional callback called before each retry */ - onRetry?: (error: unknown, attempt: number, delay: number) => void; -} - -/** - * Retry function with exponential backoff - * - * @param fn - The async function to retry - * @param options - Retry configuration options - * @returns The result of the function if it succeeds - * @throws The last error if all retries are exhausted - */ -export async function retry(fn: () => Promise, options: RetryOptions = {}): Promise { - const { - maxRetries = 3, - initialDelay = 100, - maxDelay = 5000, - backoffFactor = 2, - isRetryable = () => true, - onRetry, - } = options; - - let lastError: unknown; - let delay = initialDelay; - - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - return await fn(); - } catch (error) { - lastError = error; - - // Check if we've exhausted retries - if (attempt >= maxRetries) { - throw error; - } - - // Check if the error is retryable - if (!isRetryable(error)) { - throw error; - } - - // Calculate delay with jitter (0.5 to 1.5 of calculated delay) - const jitter = 0.5 + Math.random(); - const actualDelay = Math.min(delay * jitter, maxDelay); - - // Call onRetry callback if provided - if (onRetry) { - onRetry(error, attempt + 1, actualDelay); - } - - // Wait before next attempt - await sleep(actualDelay); - - // Increase delay for next attempt - delay = Math.min(delay * backoffFactor, maxDelay); - } - } - - // This should never be reached, but TypeScript needs it - throw lastError; -} - -/** - * Sleep for a specified duration - * - * @param ms - Duration in milliseconds - */ -export function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Debounce a function - only execute after the specified delay - * has passed without another call - * - * @param fn - The function to debounce - * @param delay - Delay in milliseconds - */ -export function debounce unknown>( - fn: T, - delay: number -): (...args: Parameters) => void { - let timeoutId: ReturnType | null = null; - - return (...args: Parameters) => { - if (timeoutId) { - clearTimeout(timeoutId); - } - timeoutId = setTimeout(() => { - fn(...args); - timeoutId = null; - }, delay); - }; -} - -/** - * Throttle a function - execute at most once per specified interval - * - * @param fn - The function to throttle - * @param interval - Minimum interval between executions in milliseconds - */ -export function throttle unknown>( - fn: T, - interval: number -): (...args: Parameters) => void { - let lastCall = 0; - let timeoutId: ReturnType | null = null; - - return (...args: Parameters) => { - const now = Date.now(); - const timeSinceLastCall = now - lastCall; - - if (timeSinceLastCall >= interval) { - lastCall = now; - fn(...args); - } else if (!timeoutId) { - timeoutId = setTimeout( - () => { - lastCall = Date.now(); - fn(...args); - timeoutId = null; - }, - interval - timeSinceLastCall - ); - } - }; -} - -/** - * Create a deferred promise that can be resolved/rejected externally - */ -export function deferred(): { - promise: Promise; - resolve: (value: T) => void; - reject: (error: unknown) => void; -} { - let resolve!: (value: T) => void; - let reject!: (error: unknown) => void; - - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - - return { promise, resolve, reject }; -} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json deleted file mode 100644 index 8f47187..0000000 --- a/packages/core/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "exactOptionalPropertyTypes": true, - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] -} diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts deleted file mode 100644 index c129321..0000000 --- a/packages/core/tsup.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/index.ts', 'src/cli.ts'], - format: ['esm'], - dts: true, - clean: true, - sourcemap: true, - target: 'node20', - shims: true, -}); diff --git a/packages/vestige-mcp-npm/package.json b/packages/vestige-mcp-npm/package.json index 00f6f8c..06a9318 100644 --- a/packages/vestige-mcp-npm/package.json +++ b/packages/vestige-mcp-npm/package.json @@ -1,6 +1,6 @@ { "name": "vestige-mcp-server", - "version": "1.0.0", + "version": "1.1.2", "description": "Vestige MCP Server - AI Memory System for Claude and other assistants", "bin": { "vestige-mcp": "bin/vestige-mcp.js", diff --git a/packages/vestige-mcp-npm/scripts/postinstall.js b/packages/vestige-mcp-npm/scripts/postinstall.js index 43e0be7..a738441 100644 --- a/packages/vestige-mcp-npm/scripts/postinstall.js +++ b/packages/vestige-mcp-npm/scripts/postinstall.js @@ -7,7 +7,7 @@ const os = require('os'); const { execSync } = require('child_process'); const VERSION = require('../package.json').version; -const BINARY_VERSION = '1.1.0'; // GitHub release version for binaries +const BINARY_VERSION = '1.1.2'; // GitHub release version for binaries const PLATFORM = os.platform(); const ARCH = os.arch(); @@ -109,7 +109,7 @@ function extract(archivePath, destDir) { function makeExecutable(binDir) { if (isWindows) return; - const binaries = ['vestige-mcp', 'vestige']; + const binaries = ['vestige-mcp', 'vestige', 'vestige-restore']; for (const bin of binaries) { const binPath = path.join(binDir, bin); if (fs.existsSync(binPath)) {