diff --git a/.env.example b/.env.example index fd605b32..4a842b92 100644 --- a/.env.example +++ b/.env.example @@ -119,6 +119,7 @@ AWS_REGION=us-east-1 # Ollama Local Embeddings OLLAMA_URL=http://localhost:11434 +OM_OLLAMA_MODEL=bge-m3:latest # Local Model Path (for custom embedding models) LOCAL_MODEL_PATH=/path/to/your/local/model @@ -167,6 +168,57 @@ OM_KEYWORD_MIN_LENGTH=3 OM_MIN_SCORE=0.3 +# -------------------------------------------- +# Optional FalkorDB GraphRAG bridge +# -------------------------------------------- +# Keep disabled until FalkorDB + tools/openmemory-graphrag-bridge are running. +OM_GRAPHRAG_ENABLED=false +OM_GRAPHRAG_URL=http://127.0.0.1:8765 +# Local GraphRAG + Ollama can take well over 15s on first ingest/query. +OM_GRAPHRAG_TIMEOUT_MS=120000 +OM_GRAPHRAG_BRIDGE_API_KEY= + +# Explicit write gate for /graphrag/sync and GraphRAG mirroring. +OM_GRAPHRAG_WRITE_ENABLED=false +# Keep false unless the server is strictly loopback/firewalled and the caller is trusted. +OM_GRAPHRAG_ALLOW_UNAUTH_WRITE=false +# Keep false unless the graph is intentionally global and safe to query without user/project filtering. +OM_GRAPHRAG_ALLOW_GLOBAL_QUERY=false +# Legacy compatibility flag. Ordinary scoped queries no longer require this once +# the graph has structured scope properties on Document/Chunk nodes. +OM_GRAPHRAG_ALLOW_UNFILTERED_SCOPED_QUERY=false + +# Best-effort mirror of new /memory/add writes into GraphRAG. +# Keep false until redaction/allowlist policy is explicit for the source. +OM_GRAPHRAG_SYNC_ON_ADD=false + +# Include GraphRAG bridge output in /api/ide/context responses. +OM_GRAPHRAG_CONTEXT_ENABLED=false + +FALKORDB_HOST=127.0.0.1 +FALKORDB_PORT=6379 +OM_GRAPHRAG_GRAPH_NAME=openmemory +OM_GRAPHRAG_LLM_MODEL= +# Optional guardrail for the GraphRAG LLM completion path. +OM_GRAPHRAG_LLM_MAX_TOKENS= +OM_GRAPHRAG_EMBEDDER_MODEL= +# Match this to the chosen embedder output dimension. Example: +# local `ollama/bge-m3:latest` returns 1024 dimensions. +OM_GRAPHRAG_EMBEDDER_DIMENSIONS=256 +OLLAMA_API_BASE=http://host.docker.internal:11434 +HF_HOME=/root/.cache/huggingface +TRANSFORMERS_CACHE=/root/.cache/huggingface/transformers +OM_GRAPHRAG_GLINER_MODEL=urchade/gliner_medium-v2.1 +OM_GRAPHRAG_GLINER_CACHE_DIR=/root/.cache/huggingface +OM_GRAPHRAG_GLINER_PREWARM=true +# Keep false for deterministic cached startup. Set true only if you explicitly +# want startup-time Hugging Face network fallback when the local cache misses or +# the cached snapshot cannot be loaded. +OM_GRAPHRAG_GLINER_ALLOW_ONLINE_FALLBACK=false +# Keep false for normal operation. Set true only if you intentionally need the +# stale-corpus compatibility fallback during scoped queries before backfill. +OM_GRAPHRAG_ENABLE_LEGACY_SCOPE_RECOVERY=false + # ============================================ # Smart Decay Settings (Time-Based Algorithm) # ============================================ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87c047de..89ecbfa2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,9 @@ jobs: OM_VEC_DIM: "1536" steps: - uses: actions/checkout@v4 + + - name: Check models mirror + run: node tools/sync-openmemory-models.mjs --check - name: Set up Node.js uses: actions/setup-node@v4 @@ -57,3 +60,22 @@ jobs: run: | cd packages/openmemory-js npx tsx tests/test_omnibus.ts + + test-graphrag-bridge: + name: Test GraphRAG bridge + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Build GraphRAG bridge image + run: | + docker build -t openmemory-graphrag-bridge:latest tools/openmemory-graphrag-bridge + + - name: Run hermetic bridge E2E + run: | + python tools/openmemory-graphrag-bridge/tests/test_e2e_bridge_stack.py diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 54f76715..7d9d97b5 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -20,6 +20,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Check models mirror + run: node tools/sync-openmemory-models.mjs --check + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -47,7 +50,7 @@ jobs: run: | max_attempts=30 attempt=0 - until curl -f http://localhost:8080/health || [ $attempt -eq $max_attempts ]; do + until curl -f http://localhost:8180/health || [ $attempt -eq $max_attempts ]; do echo "Waiting for service to be healthy... (attempt $((attempt+1))/$max_attempts)" sleep 2 attempt=$((attempt+1)) @@ -60,7 +63,7 @@ jobs: fi echo "✅ Service is healthy!" - curl -v http://localhost:8080/health + curl -v http://localhost:8180/health - name: Show Container Logs if: failure() diff --git a/README.md b/README.md index c69ef090..3dc3f8e0 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ OpenMemory is a **cognitive memory engine** for LLMs and agents. - 🧩 Integrations: LangChain, CrewAI, AutoGen, Streamlit, MCP, VS Code - 📥 Sources: GitHub, Notion, Google Drive, OneDrive, Web Crawler - 🔍 Explainable traces (see *why* something was recalled) +- 🕸 Optional FalkorDB GraphRAG sidecar for relationship-heavy retrieval Your model stays stateless. **Your app stops being amnesiac.** @@ -78,6 +79,28 @@ See the integrations section in the docs for concrete patterns. --- +### Optional FalkorDB GraphRAG + +OpenMemory can mirror selected memories into a FalkorDB GraphRAG-SDK sidecar for +relationship-heavy and multi-hop retrieval. The existing HSG and temporal memory +systems stay enabled, and GraphRAG is off by default. + +When enabled, OpenMemory uses the GraphRAG SDK incremental document APIs for +upserts and deletes, so memory updates can be propagated without rebuilding the +entire graph. Finalization remains explicit and should be batched deliberately. + +See [docs/graphrag.md](docs/graphrag.md) for the bridge, Docker profile, API, +and MCP tools. + +### Codex IDE/CLI autouse + +Codex can use OpenMemory through MCP, the IDE context provider, and bounded +redacted hooks that store session/prompt events through `/api/ide/events`. + +See [docs/codex-autouse.md](docs/codex-autouse.md). + +--- + ### 🟦 Node / JavaScript (local-first) Install: diff --git a/docker-compose.yml b/docker-compose.yml index 67eafe8a..70d0814e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,10 @@ services: context: ./packages/openmemory-js dockerfile: Dockerfile ports: - - '8080:8080' + - '127.0.0.1:8180:8180' environment: # Core Configuration - - OM_PORT=${OM_PORT:-8080} + - OM_PORT=${OM_PORT:-8180} - OM_MODE=${OM_MODE:-standard} - OM_TIER=${OM_TIER:-hybrid} - OM_DB_PATH=${OM_DB_PATH:-/data/openmemory.sqlite} @@ -51,6 +51,7 @@ services: # Ollama Provider - OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434} - OM_OLLAMA_URL=${OM_OLLAMA_URL:-http://localhost:11434} + - OM_OLLAMA_MODEL=${OM_OLLAMA_MODEL:-} # Local Model - LOCAL_MODEL_PATH=${LOCAL_MODEL_PATH:-} @@ -116,6 +117,18 @@ services: # Keyword Extraction - OM_KEYWORD_BOOST=${OM_KEYWORD_BOOST:-2.5} - OM_KEYWORD_MIN_LENGTH=${OM_KEYWORD_MIN_LENGTH:-3} + + # Optional FalkorDB GraphRAG bridge + - OM_GRAPHRAG_ENABLED=${OM_GRAPHRAG_ENABLED:-false} + - OM_GRAPHRAG_URL=${OM_GRAPHRAG_URL:-http://graphrag-bridge:8765} + - OM_GRAPHRAG_TIMEOUT_MS=${OM_GRAPHRAG_TIMEOUT_MS:-120000} + - OM_GRAPHRAG_BRIDGE_API_KEY=${OM_GRAPHRAG_BRIDGE_API_KEY:-} + - OM_GRAPHRAG_WRITE_ENABLED=${OM_GRAPHRAG_WRITE_ENABLED:-false} + - OM_GRAPHRAG_ALLOW_UNAUTH_WRITE=${OM_GRAPHRAG_ALLOW_UNAUTH_WRITE:-false} + - OM_GRAPHRAG_ALLOW_GLOBAL_QUERY=${OM_GRAPHRAG_ALLOW_GLOBAL_QUERY:-false} + - OM_GRAPHRAG_ALLOW_UNFILTERED_SCOPED_QUERY=${OM_GRAPHRAG_ALLOW_UNFILTERED_SCOPED_QUERY:-false} + - OM_GRAPHRAG_SYNC_ON_ADD=${OM_GRAPHRAG_SYNC_ON_ADD:-false} + - OM_GRAPHRAG_CONTEXT_ENABLED=${OM_GRAPHRAG_CONTEXT_ENABLED:-false} volumes: - openmemory_data:/data restart: unless-stopped @@ -125,7 +138,7 @@ services: 'CMD', 'node', '-e', - 'require("http").get("http://localhost:8080/health",(res)=>process.exit(res.statusCode===200?0:1)).on("error",()=>process.exit(1))', + 'require("http").get("http://localhost:8180/health",(res)=>process.exit(res.statusCode===200?0:1)).on("error",()=>process.exit(1))', ] interval: 30s timeout: 10s @@ -140,12 +153,12 @@ services: context: ./dashboard dockerfile: Dockerfile args: - - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8080} + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8180} - NEXT_PUBLIC_API_KEY=${NEXT_PUBLIC_API_KEY:-} ports: - '3000:3000' environment: - - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8080} + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8180} - NEXT_PUBLIC_API_KEY=${NEXT_PUBLIC_API_KEY:-} depends_on: openmemory: @@ -175,9 +188,58 @@ services: # retries: 3 # start_period: 30s + # Optional: FalkorDB + GraphRAG-SDK bridge for relationship-heavy retrieval. + # Start with: docker compose --profile graphrag up -d falkordb graphrag-bridge openmemory + falkordb: + profiles: + - graphrag + image: falkordb/falkordb:latest + ports: + - '127.0.0.1:${FALKORDB_HOST_PORT:-6379}:6379' + - '127.0.0.1:${FALKORDB_DASHBOARD_PORT:-3001}:3000' + volumes: + - falkordb_data:/data + restart: unless-stopped + + graphrag-bridge: + profiles: + - graphrag + build: + context: ./tools/openmemory-graphrag-bridge + dockerfile: Dockerfile + ports: + - '127.0.0.1:${GRAPHRAG_BRIDGE_HOST_PORT:-8765}:8765' + environment: + - FALKORDB_HOST=falkordb + - FALKORDB_PORT=6379 + - OM_GRAPHRAG_BRIDGE_API_KEY=${OM_GRAPHRAG_BRIDGE_API_KEY:-} + - OM_GRAPHRAG_GRAPH_NAME=${OM_GRAPHRAG_GRAPH_NAME:-openmemory} + - OM_GRAPHRAG_LLM_MODEL=${OM_GRAPHRAG_LLM_MODEL:-} + - OM_GRAPHRAG_LLM_MAX_TOKENS=${OM_GRAPHRAG_LLM_MAX_TOKENS:-} + - OM_GRAPHRAG_EMBEDDER_MODEL=${OM_GRAPHRAG_EMBEDDER_MODEL:-} + - OM_GRAPHRAG_EMBEDDER_DIMENSIONS=${OM_GRAPHRAG_EMBEDDER_DIMENSIONS:-256} + - OLLAMA_API_BASE=${OLLAMA_API_BASE:-http://host.docker.internal:11434} + - OM_GRAPHRAG_API_BASE=${OM_GRAPHRAG_API_BASE:-} + - HF_HOME=${HF_HOME:-/root/.cache/huggingface} + - TRANSFORMERS_CACHE=${TRANSFORMERS_CACHE:-/root/.cache/huggingface/transformers} + - OM_GRAPHRAG_GLINER_MODEL=${OM_GRAPHRAG_GLINER_MODEL:-urchade/gliner_medium-v2.1} + - OM_GRAPHRAG_GLINER_CACHE_DIR=${OM_GRAPHRAG_GLINER_CACHE_DIR:-/root/.cache/huggingface} + - OM_GRAPHRAG_GLINER_PREWARM=${OM_GRAPHRAG_GLINER_PREWARM:-true} + - OM_GRAPHRAG_GLINER_ALLOW_ONLINE_FALLBACK=${OM_GRAPHRAG_GLINER_ALLOW_ONLINE_FALLBACK:-false} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + volumes: + - graphrag_bridge_hf_cache:/root/.cache/huggingface + depends_on: + - falkordb + restart: unless-stopped + volumes: openmemory_data: driver: local + falkordb_data: + driver: local + graphrag_bridge_hf_cache: + driver: local # Uncomment if using Valkey # valkey_data: # driver: local diff --git a/docs/codex-autouse.md b/docs/codex-autouse.md new file mode 100644 index 00000000..80b3b573 --- /dev/null +++ b/docs/codex-autouse.md @@ -0,0 +1,55 @@ +# Codex IDE/CLI Autouse + +OpenMemory can be used by Codex through three layers: + +1. MCP tools via `~/.codex/config.toml`: + `mcp_servers.openmemory.url = "http://localhost:8180/mcp"` plus + `bearer_token_env_var = "OPENMEMORY_API_KEY"` when server auth is enabled. +2. IDE context provider via `~/.codex/context.json`: + preferably through a local proxy such as + `http://127.0.0.1:8181/api/ide/context`, so `context.json` does not need to + carry a literal API key. +3. Codex hooks via `~/.codex/hooks/openmemory-active-bridge.js`. + +The hook bridge is bounded and fail-open. It checks `/health`, emits +SessionStart `additionalContext` when OpenMemory is up, tries one bounded +`/api/ide/context` recall for the current project, falls back to recent +project memories through the local MCP surface when semantic recall is empty, +and stores only small redacted/truncated session or prompt events through +`/api/ide/events`. + +## Safety Defaults + +- High-confidence token-like strings are redacted before storage. +- Prompt excerpts are capped by `OPENMEMORY_HOOK_MAX_CONTENT` (`900` by + default). +- The hook uses `OPENMEMORY_API_KEY` or `OM_API_KEY` when present, and falls + back to `~/.codex/openmemory-api-key.txt` for local no-restart operation. +- A small local context proxy can read the same key file and inject `x-api-key` + for IDE context calls, removing the plaintext secret from `context.json`. +- Any timeout or OpenMemory failure exits `0` and does not block Codex. +- GraphRAG writes remain controlled separately by `OM_GRAPHRAG_WRITE_ENABLED` + and the authenticated write gate. + +## Smoke + +```powershell +node --check C:/Users/Михаило/.codex/hooks/openmemory-active-bridge.js +Invoke-RestMethod -Uri http://127.0.0.1:8180/health -TimeoutSec 5 +``` + +Manual redaction smoke: + +```powershell +$payload = '{"session_id":"codex-hook-smoke","cwd":"D:/BooksDocs/Project Astra","prompt":"Smoke sk-REDACTED-EXAMPLE"}' +$payload | node C:/Users/Михаило/.codex/hooks/openmemory-active-bridge.js prompt +``` + +Then query: + +```powershell +Invoke-RestMethod -Uri http://127.0.0.1:8181/api/ide/context ` + -Method Post ` + -ContentType 'application/json' ` + -Body '{"query":"OpenMemory hook redacted excerpt","project_id":"D:/BooksDocs/Project Astra","user_id":"codex","k":5}' +``` diff --git a/docs/graphrag.md b/docs/graphrag.md new file mode 100644 index 00000000..172e2c26 --- /dev/null +++ b/docs/graphrag.md @@ -0,0 +1,142 @@ +# FalkorDB GraphRAG Integration + +OpenMemory keeps its existing HSG and temporal memory systems as the primary +runtime. FalkorDB GraphRAG is an optional sidecar for relationship-heavy, +multi-hop retrieval. + +## Architecture + +```text +Codex IDE / Codex CLI + | + v +OpenMemory Node runtime + - /memory/add + - /api/ide/context + - /mcp tools + | + | optional HTTP bridge, env-gated + v +tools/openmemory-graphrag-bridge + | + v +FalkorDB + graphrag-sdk +``` + +The bridge is disabled by default. Normal OpenMemory startup, MCP, and IDE +context retrieval must work even when FalkorDB and `graphrag-sdk` are absent. + +## Enable + +1. Start FalkorDB and the bridge: + +```bash +docker compose --profile graphrag up -d falkordb graphrag-bridge +``` + +2. Enable OpenMemory integration: + +```bash +OM_GRAPHRAG_ENABLED=true +OM_GRAPHRAG_URL=http://127.0.0.1:8765 +OM_GRAPHRAG_BRIDGE_API_KEY= +OM_GRAPHRAG_TIMEOUT_MS=120000 +OM_GRAPHRAG_LLM_MODEL= +OM_GRAPHRAG_LLM_MAX_TOKENS= +OM_GRAPHRAG_EMBEDDER_MODEL= +OM_GRAPHRAG_EMBEDDER_DIMENSIONS= +``` + +The bridge does not choose an external LLM or embedder by default. Set these +models explicitly, preferably to a local provider when syncing private memory. +For local Ollama models in Docker, also set `OLLAMA_API_BASE=http://host.docker.internal:11434`. +Example: `ollama/bge-m3:latest` needs `OM_GRAPHRAG_EMBEDDER_DIMENSIONS=1024`. +If local Ollama verify/relationship extraction is too slow on cold starts, +use `OM_GRAPHRAG_LLM_MAX_TOKENS` as a guardrail for the bridge LLM completion +path. +For local Ollama-backed ingest/query, raise `OM_GRAPHRAG_TIMEOUT_MS`; `120000` +is a practical starting point for cold caches and first-time extraction. +The bridge can also persist Hugging Face / GLiNER artifacts across container +recreates through `HF_HOME`, `TRANSFORMERS_CACHE`, and the +`graphrag_bridge_hf_cache` volume, with optional background prewarm via +`OM_GRAPHRAG_GLINER_PREWARM=true`. +Keep `OM_GRAPHRAG_GLINER_ALLOW_ONLINE_FALLBACK=false` for deterministic cached +startup; enable it only when you intentionally want startup-time HF fallback on +cache miss or cached-model load failure. + +3. Optional writes, auto-sync, and IDE context: + +```bash +OM_GRAPHRAG_WRITE_ENABLED=true +OM_API_KEY= +OM_GRAPHRAG_SYNC_ON_ADD=true +OM_GRAPHRAG_CONTEXT_ENABLED=true +``` + +Use writes and auto-sync only after redaction and source allowlist policy is +explicit. If `OM_API_KEY` is empty, GraphRAG writes stay blocked unless +`OM_GRAPHRAG_ALLOW_UNAUTH_WRITE=true` is set deliberately for a strictly +loopback/firewalled deployment. + +Scoped IDE/MCP GraphRAG queries are enforced server-side in the bridge. When +`user_id` and/or `project_id` are supplied, the bridge filters retrieved source +passages by structured `openmemory_*` properties stamped onto `Document` and +`Chunk` nodes inside FalkorDB before answer generation and fails closed if no +scope-matching context remains. Pre-existing graph content without those +properties falls back to the older provenance-text check until it is re-synced. +The bridge also exposes `POST /scope/backfill` for operator-driven backfill of +older graph content from the already-ingested provenance text. +For local operator use on Windows/PowerShell, the bridge tool directory ships +`backfill-scope.ps1`, `run-tests.ps1`, and `run-e2e.ps1`. +`run-e2e.ps1` now defaults to a hermetic bridge smoke with a local fake +Ollama-compatible stub so the test can run without host Ollama or live HF +downloads; use `-UseHostOllama` only when you intentionally want the +operator-realistic path. +`OM_GRAPHRAG_ENABLE_LEGACY_SCOPE_RECOVERY=true` is now an operator-only escape + hatch for stale corpora; normal scoped queries should run with it disabled + once scope metadata backfill has been completed. + +`OM_GRAPHRAG_ALLOW_UNFILTERED_SCOPED_QUERY` is now a legacy compatibility flag +and is no longer required for ordinary scoped queries. + +Bridge `/health` reports the **effective** storage contract in +`scope_storage_contract`. It now also exposes +`scope_operator_recovery_path_present=true` to signal that dormant +operator-only stale-corpus recovery code still exists. +`scope_storage_compatibility_path_present` remains as a deprecated alias for +older local consumers. + +Unscoped/global GraphRAG queries are also disabled by default. Set +`OM_GRAPHRAG_ALLOW_GLOBAL_QUERY=true` only when global graph retrieval is an +explicit policy decision. + +Direct bridge access requires `OM_GRAPHRAG_BRIDGE_API_KEY`; the Node runtime +sends it as `x-graph-api-key`. + +The current mirror uses GraphRAG SDK incremental document APIs: +`update(..., if_missing="ingest")` for upserts and `delete_document(...)` for +deletes. `finalize()` remains caller-controlled because its cost scales with +graph size; do not enable per-write finalization unless you have measured the +trade-off. + +## API + +- `GET /graphrag/status` checks bridge reachability. +- `POST /graphrag/sync` syncs either an existing OpenMemory memory id or + explicit `{ document_id, content }`; it requires `OM_GRAPHRAG_WRITE_ENABLED=true`. +- `POST /graphrag/delete` removes one GraphRAG document by `document_id` or + OpenMemory memory id. +- `POST /graphrag/query` queries GraphRAG and returns the raw bridge payload. + Its `k` field is mapped to the GraphRAG SDK retrieval caps used by the + bridge strategy. + +## MCP Tools + +- `openmemory_graphrag_status` +- `openmemory_graphrag_sync` +- `openmemory_graphrag_delete` +- `openmemory_graphrag_query` + +These tools are supplemental. Use `openmemory_query` first for ordinary +personal/project memory lookup, and use GraphRAG for questions where graph +relationships matter. diff --git a/models.yml b/models.yml index ed466804..16072879 100644 --- a/models.yml +++ b/models.yml @@ -6,41 +6,42 @@ # Each sector can use different models optimized for that type of memory episodic: - ollama: nomic-embed-text + ollama: bge-m3:latest openai: text-embedding-3-small gemini: models/text-embedding-004 aws: amazon.titan-embed-text-v2:0 local: all-MiniLM-L6-v2 semantic: - ollama: nomic-embed-text + ollama: bge-m3:latest openai: text-embedding-3-small gemini: models/text-embedding-004 aws: amazon.titan-embed-text-v2:0 local: all-MiniLM-L6-v2 procedural: - ollama: nomic-embed-text + ollama: bge-m3:latest openai: text-embedding-3-small gemini: models/text-embedding-004 aws: amazon.titan-embed-text-v2:0 local: all-MiniLM-L6-v2 emotional: - ollama: nomic-embed-text + ollama: bge-m3:latest openai: text-embedding-3-small gemini: models/text-embedding-004 aws: amazon.titan-embed-text-v2:0 local: all-MiniLM-L6-v2 reflective: - ollama: nomic-embed-text + ollama: bge-m3:latest openai: text-embedding-3-large gemini: models/text-embedding-004 aws: amazon.titan-embed-text-v2:0 local: all-mpnet-base-v2 # Available Ollama models (pull with: ollama pull ) -# - nomic-embed-text (768d, recommended) +# - bge-m3:latest (1024d, current local live mapping) +# - nomic-embed-text (768d) # - mxbai-embed-large (1024d) # - snowflake-arctic-embed (1024d) # - all-minilm (384d, fast) @@ -55,4 +56,3 @@ reflective: #AWS models: # - amazon.titan-embed-text-v2:0 (1024d, 512d, 256) - diff --git a/packages/openmemory-js/Dockerfile b/packages/openmemory-js/Dockerfile index 5f65fbc0..0a817a98 100644 --- a/packages/openmemory-js/Dockerfile +++ b/packages/openmemory-js/Dockerfile @@ -6,12 +6,19 @@ WORKDIR /app # Copy package manifests to install dependencies COPY package*.json ./ +# sqlite3 native binaries from npm can target a newer glibc than this base +# image. Build it inside the container to match the runtime ABI. +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ \ + && rm -rf /var/lib/apt/lists/* + # Install all dependencies (including devDependencies) for build -RUN npm ci +RUN npm_config_build_from_source=sqlite3 npm ci # Copy source code and build the application COPY src/ ./src/ COPY tsconfig.json ./ +COPY models.yml ./ RUN NODE_OPTIONS="--max-old-space-size=4096" npm run build # Remove devDependencies to reduce image size for the runtime image @@ -30,6 +37,7 @@ RUN groupadd --gid 1001 appgroup \ # Copy only production artifacts from the builder stage COPY --from=builder /app/node_modules ./node_modules/ COPY --from=builder /app/dist ./dist/ +COPY --from=builder /app/models.yml ./models.yml COPY package.json ./ # Create a directory for persistent data and secure file permissions @@ -40,12 +48,15 @@ RUN mkdir -p /data \ # Switch to non-root user USER appuser +# Match the local compose default port. +ENV OM_PORT=8180 + # Expose the application port -EXPOSE 8080 +EXPOSE 8180 # Define a lightweight health check that verifies the /health endpoint HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD node -e "require('http').get('http://localhost:8080/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" + CMD node -e "const port = process.env.OM_PORT || 8180; require('http').get('http://localhost:' + port + '/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" # Start the application using npm ENTRYPOINT ["npm", "start"] diff --git a/packages/openmemory-js/models.yml b/packages/openmemory-js/models.yml new file mode 100644 index 00000000..16072879 --- /dev/null +++ b/packages/openmemory-js/models.yml @@ -0,0 +1,58 @@ +# OpenMemory Embedding Models Configuration +# Configure which models to use for each brain sector and provider +# Provider settings (URLs, API keys, etc.) are in .env + +# Sector-specific model mappings +# Each sector can use different models optimized for that type of memory + +episodic: + ollama: bge-m3:latest + openai: text-embedding-3-small + gemini: models/text-embedding-004 + aws: amazon.titan-embed-text-v2:0 + local: all-MiniLM-L6-v2 + +semantic: + ollama: bge-m3:latest + openai: text-embedding-3-small + gemini: models/text-embedding-004 + aws: amazon.titan-embed-text-v2:0 + local: all-MiniLM-L6-v2 + +procedural: + ollama: bge-m3:latest + openai: text-embedding-3-small + gemini: models/text-embedding-004 + aws: amazon.titan-embed-text-v2:0 + local: all-MiniLM-L6-v2 + +emotional: + ollama: bge-m3:latest + openai: text-embedding-3-small + gemini: models/text-embedding-004 + aws: amazon.titan-embed-text-v2:0 + local: all-MiniLM-L6-v2 + +reflective: + ollama: bge-m3:latest + openai: text-embedding-3-large + gemini: models/text-embedding-004 + aws: amazon.titan-embed-text-v2:0 + local: all-mpnet-base-v2 +# Available Ollama models (pull with: ollama pull ) +# - bge-m3:latest (1024d, current local live mapping) +# - nomic-embed-text (768d) +# - mxbai-embed-large (1024d) +# - snowflake-arctic-embed (1024d) +# - all-minilm (384d, fast) + +# OpenAI models: +# - text-embedding-3-small (1536d) +# - text-embedding-3-large (3072d) + +# Gemini models: +# - models/text-embedding-004 (768d) - latest +# - models/embedding-001 (768d) - deprecated + +#AWS models: +# - amazon.titan-embed-text-v2:0 (1024d, 512d, 256) diff --git a/packages/openmemory-js/package-lock.json b/packages/openmemory-js/package-lock.json index 32e5ce10..894b9924 100644 --- a/packages/openmemory-js/package-lock.json +++ b/packages/openmemory-js/package-lock.json @@ -1,16 +1,16 @@ { "name": "openmemory-js", - "version": "1.3.3", + "version": "1.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openmemory-js", - "version": "1.3.3", + "version": "1.3.4", "dependencies": { - "@aws-sdk/client-bedrock-runtime": "^3.932.0", + "@aws-sdk/client-bedrock-runtime": "^3.1046.0", "@azure/msal-node": "^2.0.0", - "@modelcontextprotocol/sdk": "^1.27.1", + "@modelcontextprotocol/sdk": "^1.29.0", "@notionhq/client": "^2.2.0", "@octokit/rest": "^21.0.0", "cheerio": "^1.0.0", @@ -22,7 +22,7 @@ "openai": "^4.73.0", "pdf-parse": "^2.4.5", "pg": "^8.16.3", - "sqlite3": "^5.1.7", + "sqlite3": "^6.0.1", "turndown": "^7.2.2", "ws": "^8.18.3", "zod": "^3.25.76" @@ -70,44 +70,6 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", @@ -142,421 +104,308 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.932.0.tgz", - "integrity": "sha512-wPG/sL9EELt6+y3AC2vs/LznGgJbUbuB5Wmchqrq67d+3eLcd3H4/OO5ahhhVhGx7sUijOEKkdCraEqilhC8LQ==", + "version": "3.1046.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1046.0.tgz", + "integrity": "sha512-MT+nf7bna9gEohYFqUbPivR/XFPq7K8PmvgZY0h+kC/4k9SYDjQjsuA+sGH2AaZwJmMGXhmL4FpWRsYMc5gOQw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.932.0", - "@aws-sdk/credential-provider-node": "3.932.0", - "@aws-sdk/eventstream-handler-node": "3.930.0", - "@aws-sdk/middleware-eventstream": "3.930.0", - "@aws-sdk/middleware-host-header": "3.930.0", - "@aws-sdk/middleware-logger": "3.930.0", - "@aws-sdk/middleware-recursion-detection": "3.930.0", - "@aws-sdk/middleware-user-agent": "3.932.0", - "@aws-sdk/middleware-websocket": "3.930.0", - "@aws-sdk/region-config-resolver": "3.930.0", - "@aws-sdk/token-providers": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@aws-sdk/util-endpoints": "3.930.0", - "@aws-sdk/util-user-agent-browser": "3.930.0", - "@aws-sdk/util-user-agent-node": "3.932.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.2", - "@smithy/eventstream-serde-browser": "^4.2.5", - "@smithy/eventstream-serde-config-resolver": "^4.3.5", - "@smithy/eventstream-serde-node": "^4.2.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.9", - "@smithy/middleware-retry": "^4.4.9", - "@smithy/middleware-serde": "^4.2.5", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.8", - "@smithy/util-defaults-mode-node": "^4.2.11", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-stream": "^4.5.6", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/credential-provider-node": "^3.972.40", + "@aws-sdk/eventstream-handler-node": "^3.972.15", + "@aws-sdk/middleware-eventstream": "^3.972.11", + "@aws-sdk/middleware-host-header": "^3.972.11", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.12", + "@aws-sdk/middleware-user-agent": "^3.972.39", + "@aws-sdk/middleware-websocket": "^3.972.17", + "@aws-sdk/region-config-resolver": "^3.972.14", + "@aws-sdk/token-providers": "3.1046.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.9", + "@aws-sdk/util-user-agent-browser": "^3.972.11", + "@aws-sdk/util-user-agent-node": "^3.973.25", + "@smithy/core": "^3.24.1", + "@smithy/fetch-http-handler": "^5.4.1", + "@smithy/node-http-handler": "^4.7.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.932.0.tgz", - "integrity": "sha512-XHqHa5iv2OQsKoM2tUQXs7EAyryploC00Wg0XSFra/KAKqyGizUb5XxXsGlyqhebB29Wqur+zwiRwNmejmN0+Q==", + "node_modules/@aws-sdk/core": { + "version": "3.974.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.9.tgz", + "integrity": "sha512-bXxosFunr+v/kqNb99r1NRkrVBha7CG036fRSpWGbC1A/e363XFQN6wcZMx7MYTdRr1tYwNnkrWX2xc1rT3BCQ==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.932.0", - "@aws-sdk/middleware-host-header": "3.930.0", - "@aws-sdk/middleware-logger": "3.930.0", - "@aws-sdk/middleware-recursion-detection": "3.930.0", - "@aws-sdk/middleware-user-agent": "3.932.0", - "@aws-sdk/region-config-resolver": "3.930.0", - "@aws-sdk/types": "3.930.0", - "@aws-sdk/util-endpoints": "3.930.0", - "@aws-sdk/util-user-agent-browser": "3.930.0", - "@aws-sdk/util-user-agent-node": "3.932.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.2", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.9", - "@smithy/middleware-retry": "^4.4.9", - "@smithy/middleware-serde": "^4.2.5", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.8", - "@smithy/util-defaults-mode-node": "^4.2.11", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.23", + "@smithy/core": "^3.24.1", + "@smithy/signature-v4": "^5.4.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/core": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.932.0.tgz", - "integrity": "sha512-AS8gypYQCbNojwgjvZGkJocC2CoEICDx9ZJ15ILsv+MlcCVLtUJSRSx3VzJOUY2EEIaGLRrPNlIqyn/9/fySvA==", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.35.tgz", + "integrity": "sha512-WkFQ8BedszVomhh/Zzs8WwnE/XBmTqZjoQVB8u/4zH6kZCjouXZpPpb93gD8m0EZmzAl7dxHE/y+yDpuKzNCMw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.2", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.932.0.tgz", - "integrity": "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA==", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.37.tgz", + "integrity": "sha512-ylx0ZJTU+2eNcvXQ69VNR3TVSYa/ibpvdK717/NxqR9aXRMn2QRWZaiI8aa5yY/fOWZ5mknSmxGaVxxtdwv3EA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/fetch-http-handler": "^5.4.1", + "@smithy/node-http-handler": "^4.7.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.932.0.tgz", - "integrity": "sha512-b6N9Nnlg8JInQwzBkUq5spNaXssM3h3zLxGzpPrnw0nHSIWPJPTbZzA5Ca285fcDUFuKP+qf3qkuqlAjGOdWhg==", + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.39.tgz", + "integrity": "sha512-QhRSrdkk+Gq0AFIylpiI0N6lcJqFYV9Jtr4Luz5FpYOYbjJSfyTG6iLhnK/UPIgN1Jnon8WAmSC//16XYGvwkA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.5", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/credential-provider-env": "^3.972.35", + "@aws-sdk/credential-provider-http": "^3.972.37", + "@aws-sdk/credential-provider-login": "^3.972.39", + "@aws-sdk/credential-provider-process": "^3.972.35", + "@aws-sdk/credential-provider-sso": "^3.972.39", + "@aws-sdk/credential-provider-web-identity": "^3.972.39", + "@aws-sdk/nested-clients": "^3.997.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/credential-provider-imds": "^4.3.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.932.0.tgz", - "integrity": "sha512-ZBjSAXVGy7danZRHCRMJQ7sBkG1Dz39thYlvTiUaf9BKZ+8ymeiFhuTeV1OkWUBBnY0ki2dVZJvboTqfINhNxA==", + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.39.tgz", + "integrity": "sha512-1hU0NtC04QbFIuoBuF4aQ2A97GsSE5/A0ZJpDijwexsBREIQ4KPRYl3v/FfKCPBYsaTeGjkOFx5nLhWHY24LOw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.932.0", - "@aws-sdk/credential-provider-env": "3.932.0", - "@aws-sdk/credential-provider-http": "3.932.0", - "@aws-sdk/credential-provider-process": "3.932.0", - "@aws-sdk/credential-provider-sso": "3.932.0", - "@aws-sdk/credential-provider-web-identity": "3.932.0", - "@aws-sdk/nested-clients": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/nested-clients": "^3.997.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.932.0.tgz", - "integrity": "sha512-SEG9t2taBT86qe3gTunfrK8BxT710GVLGepvHr+X5Pw+qW225iNRaGN0zJH+ZE/j91tcW9wOaIoWnURkhR5wIg==", + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.40.tgz", + "integrity": "sha512-ZgrQaGkpyTlVSCCsffzijVg+KgftTAWYvI5Otc36J/4jNiHb+7MmBiJIR0a5AHLvifC92PiYHt5pijP0dswd1w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.932.0", - "@aws-sdk/credential-provider-http": "3.932.0", - "@aws-sdk/credential-provider-ini": "3.932.0", - "@aws-sdk/credential-provider-process": "3.932.0", - "@aws-sdk/credential-provider-sso": "3.932.0", - "@aws-sdk/credential-provider-web-identity": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/credential-provider-env": "^3.972.35", + "@aws-sdk/credential-provider-http": "^3.972.37", + "@aws-sdk/credential-provider-ini": "^3.972.39", + "@aws-sdk/credential-provider-process": "^3.972.35", + "@aws-sdk/credential-provider-sso": "^3.972.39", + "@aws-sdk/credential-provider-web-identity": "^3.972.39", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/credential-provider-imds": "^4.3.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.932.0.tgz", - "integrity": "sha512-BodZYKvT4p/Dkm28Ql/FhDdS1+p51bcZeMMu2TRtU8PoMDHnVDhHz27zASEKSZwmhvquxHrZHB0IGuVqjZUtSQ==", + "version": "3.972.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.35.tgz", + "integrity": "sha512-hNj1rAwZWT1vfz54BwH8FUWxZuqStrM25Q5LEIwn2erHPMRVAjLlpZqEbCEEqS99eEEOhdeetnS0WeNa3iYeEQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.932.0.tgz", - "integrity": "sha512-XYmkv+ltBjjmPZ6AmR1ZQZkQfD0uzG61M18/Lif3HAGxyg3dmod0aWx9aL6lj9SvxAGqzscrx5j4PkgLqjZruw==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.39.tgz", + "integrity": "sha512-mwIPNPldyCZkvHozb6E0X/vuQLN1UCjcA6MwUf1gaO7EwghCmuNZXatq0L3zptKFvPC4Nds7+WFUkifI1XmbSw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.932.0", - "@aws-sdk/core": "3.932.0", - "@aws-sdk/token-providers": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/nested-clients": "^3.997.7", + "@aws-sdk/token-providers": "3.1046.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.932.0.tgz", - "integrity": "sha512-Yw/hYNnC1KHuVIQF9PkLXbuKN7ljx70OSbJYDRufllQvej3kRwNcqQSnzI1M4KaObccqKaE6srg22DqpPy9p8w==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.39.tgz", + "integrity": "sha512-b9HT8CnpyPVn1hU14Q7ihjwSPlRzToYmRYJxRd5jNHEZ43lrIhoLaTT8JmfQQt5j5M8rTX1iN1X8mvu0SM1dXA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.932.0", - "@aws-sdk/nested-clients": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/nested-clients": "^3.997.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.930.0.tgz", - "integrity": "sha512-0busYTWsruRoUvs4Q4oSPkEQLR+eX80AtmLrW20SycC/OXdHFGj+bP3vhVWJyjDjXjjLwdLPJJhrqfHHrbrXQw==", + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.15.tgz", + "integrity": "sha512-Al7z1qKPUIRILnB3Ggd1Tz88wjSkQjDErajR7YY33mquTbeN0gTDtJtclNtyhQMWr7ZRpQk0v5/xop4fjT0sug==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@smithy/eventstream-codec": "^4.2.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.930.0.tgz", - "integrity": "sha512-9s9oJ/c+xavDyirkRmPBGQJ3jhRvyAXWvXwttZvUjbpR95Lepaj6Xtqxen3PLOHG1Z+Ma56KKqMdnFxg/Jhf6A==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.11.tgz", + "integrity": "sha512-A0Z45YInBwsAabF8jf9hEQjXDuq4gFHNNqxCYuk8iREFZ7hw0NZ6+7FFlYa11gJ+i6y79C4J6giQ7fa1EDRYxw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.930.0.tgz", - "integrity": "sha512-x30jmm3TLu7b/b+67nMyoV0NlbnCVT5DI57yDrhXAPCtdgM1KtdLWt45UcHpKOm1JsaIkmYRh2WYu7Anx4MG0g==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.11.tgz", + "integrity": "sha512-CBC6+tVYaOJo7QXgN1zJ4Ba2f3/Cpy4eRViYFimXW/O5Mn8hBmgXXzHu4vy4ubT80YWnp8aCFygr7dTOa14yQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.930.0.tgz", - "integrity": "sha512-vh4JBWzMCBW8wREvAwoSqB2geKsZwSHTa0nSt0OMOLp2PdTYIZDi0ZiVMmpfnjcx9XbS6aSluLv9sKx4RrG46A==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.930.0.tgz", - "integrity": "sha512-gv0sekNpa2MBsIhm2cjP3nmYSfI4nscx/+K9u9ybrWZBWUIC4kL2sV++bFjjUz4QxUIlvKByow3/a9ARQyCu7Q==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.12.tgz", + "integrity": "sha512-5eltYxKB4MfdQv7/VhWxRbAVQKow5dz9votRFigTYrWJHMQXwLMltlbk7KFWSZh5NDBySfmjT7Jv/DWfYCmDng==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@aws/lambda-invoke-store": "^0.1.1", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.932.0.tgz", - "integrity": "sha512-9BGTbJyA/4PTdwQWE9hAFIJGpsYkyEW20WON3i15aDqo5oRZwZmqaVageOD57YYqG8JDJjvcwKyDdR4cc38dvg==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.39.tgz", + "integrity": "sha512-MlNSvNsSVlMKKWaCzA0GP1nS4Cuq3WCXUN1vmMvd+Ctztib5kmRcpmTtKx9kikN8szAc+gcdp7uqJJervV2nQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@aws-sdk/util-endpoints": "3.930.0", - "@smithy/core": "^3.18.2", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.9", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.930.0.tgz", - "integrity": "sha512-dqMbapt2KH99HG+2+CrIq04HGLOGMiX89lqgu5RxZMBUTxEZlRoAvmpjSYKlB0Co033PwMmWqIRsJVw0KOLMjA==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.17.tgz", + "integrity": "sha512-0PRAeIunuJRAweM/YWATe0jCHx4BMzSuwry461/TgcwMc8mNShwTdXiG6eEQBD7u+nNPC8Inp9KXjSB6D7uNYg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@aws-sdk/util-format-url": "3.930.0", - "@smithy/eventstream-codec": "^4.2.5", - "@smithy/eventstream-serde-browser": "^4.2.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-hex-encoding": "^4.2.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/fetch-http-handler": "^5.4.1", + "@smithy/signature-v4": "^5.4.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -564,170 +413,148 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.932.0.tgz", - "integrity": "sha512-E2ucBfiXSpxZflHTf3UFbVwao4+7v7ctAeg8SWuglc1UMqMlpwMFFgWiSONtsf0SR3+ZDoWGATyCXOfDWerJuw==", + "version": "3.997.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.7.tgz", + "integrity": "sha512-jT2AXOODobQfTYGC2SChMSnZ/voIcRV/LHlY1suyhY1bdgP/voKkhEg8Ci1jiGQ4lBiaso5BEAV3ZWWpPTfmYA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.932.0", - "@aws-sdk/middleware-host-header": "3.930.0", - "@aws-sdk/middleware-logger": "3.930.0", - "@aws-sdk/middleware-recursion-detection": "3.930.0", - "@aws-sdk/middleware-user-agent": "3.932.0", - "@aws-sdk/region-config-resolver": "3.930.0", - "@aws-sdk/types": "3.930.0", - "@aws-sdk/util-endpoints": "3.930.0", - "@aws-sdk/util-user-agent-browser": "3.930.0", - "@aws-sdk/util-user-agent-node": "3.932.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.2", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.9", - "@smithy/middleware-retry": "^4.4.9", - "@smithy/middleware-serde": "^4.2.5", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.8", - "@smithy/util-defaults-mode-node": "^4.2.11", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/middleware-host-header": "^3.972.11", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.12", + "@aws-sdk/middleware-user-agent": "^3.972.39", + "@aws-sdk/region-config-resolver": "^3.972.14", + "@aws-sdk/signature-v4-multi-region": "^3.996.26", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.9", + "@aws-sdk/util-user-agent-browser": "^3.972.11", + "@aws-sdk/util-user-agent-node": "^3.973.25", + "@smithy/core": "^3.24.1", + "@smithy/fetch-http-handler": "^5.4.1", + "@smithy/node-http-handler": "^4.7.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.930.0.tgz", - "integrity": "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw==", + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.14.tgz", + "integrity": "sha512-VuLXVmm7+lKVxqFcOItPkXhjbJ02iUfxkxheRu41SfWf6/xrZup2A2SwHZos/LeQGu3SBHeqTQht80Uo3ienPA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.932.0.tgz", - "integrity": "sha512-43u82ulVuHK4zWhcSPyuPS18l0LNHi3QJQ1YtP2MfP8bPf5a6hMYp5e3lUr9oTDEWcpwBYtOW0m1DVmoU/3veA==", + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.26.tgz", + "integrity": "sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.932.0", - "@aws-sdk/nested-clients": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/signature-v4": "^5.4.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/types": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", - "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "node_modules/@aws-sdk/token-providers": { + "version": "3.1046.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1046.0.tgz", + "integrity": "sha512-9je8nZt+ntB8IjhpGNayU/AkBgvq/f4aFO2bH1LSNC5JX6K8zY4LUnr/ymqunePrwq+B5OVBpL7ILjYzMFSZAw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.974.9", + "@aws-sdk/nested-clients": "^3.997.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.930.0.tgz", - "integrity": "sha512-M2oEKBzzNAYr136RRc6uqw3aWlwCxqTP1Lawps9E1d2abRPvl1p1ztQmmXp1Ak4rv8eByIZ+yQyKQ3zPdRG5dw==", + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-endpoints": "^3.2.5", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/util-format-url": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.930.0.tgz", - "integrity": "sha512-FW6Im17Zc7F5WT39XUgDOjtJO95Yu8rsmeRHf7z+Y3FamtTSzH4f713BD/qMyJBrZIlFACWlok/Uuvdl5/qtMg==", + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.9.tgz", + "integrity": "sha512-ibx8Vd73rCTHekNGeXX8cpGWoBKbNAlwKHL3yjSxxttu5QnNDaSAM7/0MFYDjU31/F4lyrPoQcGirT0ew61xcg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", - "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.930.0.tgz", - "integrity": "sha512-q6lCRm6UAe+e1LguM5E4EqM9brQlDem4XDcQ87NzEvlTW6GzmNCO0w1jS0XgCFXQHjDxjdlNFX+5sRbHijwklg==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.11.tgz", + "integrity": "sha512-kq3RS6XQtHMrLFShbkem6h+8fxazB3jEIsbMC6aaSInOciRGE+eGAqTgJ+obO7Euo/pjM8thVqLiLISEH9X9DA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.930.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.932.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.932.0.tgz", - "integrity": "sha512-/kC6cscHrZL74TrZtgiIL5jJNbVsw9duGGPurmaVgoCbP7NnxyaSWEurbNV3VPNPhNE3bV3g4Ci+odq+AlsYQg==", + "version": "3.973.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.25.tgz", + "integrity": "sha512-066hKH/0nvV7x4ofV/iK9kz8r/qNfcR6rzuEOFqI2vQL/fcTTsDAbTw0jmXkyMzANK8ltQdALj19ns3zuOJiUw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.932.0", - "@aws-sdk/types": "3.930.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/middleware-user-agent": "^3.972.39", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "aws-crt": ">=1.0.0" @@ -739,23 +566,24 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", - "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.23.tgz", + "integrity": "sha512-A0YmgYFv+hTI9c17Ntvd2hSehm9bmJfkb+ggADBwVKA8H/3+Jx94SzR2qOB9bAA9WFeDqnfz9PKKQ+D+YAKomA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", - "fast-xml-parser": "5.2.5", + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", - "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -1226,17 +1054,10 @@ "node": ">=18" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "license": "MIT", - "optional": true - }, "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -1251,6 +1072,18 @@ "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", "license": "MIT" }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@mixmark-io/domino": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", @@ -1258,9 +1091,9 @@ "license": "BSD-2-Clause" }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", - "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -1481,6 +1314,18 @@ "node": ">= 10" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@notionhq/client": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-2.3.0.tgz", @@ -1494,32 +1339,6 @@ "node": ">=12" } }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "license": "MIT", - "optional": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@octokit/auth-token": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", @@ -1704,733 +1523,196 @@ "@octokit/openapi-types": "^25.1.0" } }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", - "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "node_modules/@smithy/core": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.2.tgz", + "integrity": "sha512-IKS7qX59fAGCYBmt5JChcDswQDupZqT2Yn2ZBA3UgTlsjRNNkQzZobbn95xoAAdtTyJmBiJB3Y02qR3rgy3Zog==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", - "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "node_modules/@smithy/credential-provider-imds": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.2.tgz", + "integrity": "sha512-iYr9ekBjmZ+FwkiHEopqGscBbl78X62cq3p5Dd0eC+gNd7fybNZFQQdDuOQjTVmFymleuA8YRWZnuXWZ8B3kKA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/core": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.4.tgz", - "integrity": "sha512-o5tMqPZILBvvROfC8vC+dSVnWJl9a0u9ax1i1+Bq8515eYjUJqqk5XjjEsDLoeL5dSqGSh6WGdVx1eJ1E/Nwhw==", + "node_modules/@smithy/fetch-http-handler": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.2.tgz", + "integrity": "sha512-3wF40g8OOCA5BnwQUvwtzZqYBbWWftDjpAlWIUo6Yld3ZzJaMAKqg7MWQBPjE8oLaqvZQUE7tVGlZPsae6A4bQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", - "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@smithy/eventstream-codec": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.5.tgz", - "integrity": "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA==", + "node_modules/@smithy/node-http-handler": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.2.tgz", + "integrity": "sha512-EdksTZ8UXYxGUgQ4mpIKrHoaj9WVGsp66TpZuixLAz1Jex8YDLnS4RH9ktGED5aOpN0OJlEtrsC9IGt76go1eA==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.9.0", - "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.5.tgz", - "integrity": "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw==", + "node_modules/@smithy/signature-v4": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.2.tgz", + "integrity": "sha512-1km1OjdLRFuITWpCPofjFqzZ+tbeWuB72ZhcYjbjkCxZ21tTPfIs4GUxRrelMyKMLxLghGD58RENnXorU/O8cw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.5.tgz", - "integrity": "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ==", + "node_modules/@smithy/types": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.5.tgz", - "integrity": "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg==", + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.5.tgz", - "integrity": "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q==", + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", - "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", - "license": "Apache-2.0", + "node_modules/@types/cheerio": { + "version": "0.22.35", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.35.tgz", + "integrity": "sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==", + "dev": true, + "license": "MIT", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@types/node": "*" } }, - "node_modules/@smithy/hash-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", - "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", - "license": "Apache-2.0", + "node_modules/@types/fluent-ffmpeg": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz", + "integrity": "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==", + "dev": true, + "license": "MIT", "dependencies": { - "@smithy/types": "^4.9.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@types/node": "*" } }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", - "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", - "license": "Apache-2.0", + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "license": "MIT", "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "undici-types": "~6.21.0" } }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "license": "Apache-2.0", + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@types/node": "*", + "form-data": "^4.0.4" } }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", - "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", - "license": "Apache-2.0", + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=10.0.0" } }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.11.tgz", - "integrity": "sha512-eJXq9VJzEer1W7EQh3HY2PDJdEcEUnv6sKuNt4eVjyeNWcQFS4KmnY+CKkYOIR6tSqarn6bjjCqg1UB+8UJiPQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.18.4", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-middleware": "^4.2.5", - "tslib": "^2.6.2" - }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "license": "ISC", + "optional": true, "engines": { - "node": ">=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@smithy/middleware-retry": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.11.tgz", - "integrity": "sha512-EL5OQHvFOKneJVRgzRW4lU7yidSwp/vRJOe542bHgExN3KNThr1rlg0iE4k4SnA+ohC+qlUxoK+smKeAYPzfAQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/service-error-classification": "^4.2.5", - "@smithy/smithy-client": "^4.9.7", - "@smithy/types": "^4.9.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/uuid": "^1.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", - "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", - "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", - "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", - "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", - "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", - "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", - "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "@smithy/util-uri-escape": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", - "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", - "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", - "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", - "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.9.7", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.7.tgz", - "integrity": "sha512-pskaE4kg0P9xNQWihfqlTMyxyFR3CH6Sr6keHYghgyqqDXzjl2QJg5lAzuVe/LzZiOzcbcVtxKYi1/fZPt/3DA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.18.4", - "@smithy/middleware-endpoint": "^4.3.11", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", - "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", - "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.2.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.10.tgz", - "integrity": "sha512-3iA3JVO1VLrP21FsZZpMCeF93aqP3uIOMvymAT3qHIJz2YlgDeRvNUspFwCNqd/j3qqILQJGtsVQnJZICh/9YA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.7", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.13.tgz", - "integrity": "sha512-PTc6IpnpSGASuzZAgyUtaVfOFpU0jBD2mcGwrgDuHf7PlFgt5TIPxCYBDbFQs06jxgeV3kd/d/sok1pzV0nJRg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.4.3", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.7", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", - "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", - "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", - "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.2.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", - "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@types/cheerio": { - "version": "0.22.35", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.35.tgz", - "integrity": "sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/fluent-ffmpeg": { - "version": "2.1.28", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz", - "integrity": "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "20.19.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", - "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } - }, - "node_modules/@types/pg": { - "version": "8.15.6", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", - "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC", - "optional": true - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2465,19 +1747,6 @@ "node": ">= 0.6" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -2490,24 +1759,10 @@ "node": ">= 8.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "optional": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "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==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2537,38 +1792,6 @@ } } }, - "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", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2589,13 +1812,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "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", - "optional": true - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2704,22 +1920,11 @@ "license": "ISC" }, "node_modules/bowser": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", - "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2759,36 +1964,6 @@ "node": ">= 0.8" } }, - "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, "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", @@ -2857,26 +2032,16 @@ "domutils": "^3.0.1" }, "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "optional": true, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=6" + "node": ">=18" } }, "node_modules/cluster-key-slot": { @@ -2888,16 +2053,6 @@ "node": ">=0.10.0" } }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2910,20 +2065,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT", - "optional": true - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC", - "optional": true - }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -3075,13 +2216,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT", - "optional": true - }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -3220,13 +2354,6 @@ "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", - "optional": true - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3236,16 +2363,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -3290,13 +2407,6 @@ "node": ">=6" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "license": "MIT", - "optional": true - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3438,6 +2548,13 @@ "node": ">=6" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0", + "optional": true + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -3482,12 +2599,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", - "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -3528,9 +2645,9 @@ "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==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -3543,10 +2660,26 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", "funding": [ { "type": "github", @@ -3555,12 +2688,33 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "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", @@ -3694,25 +2848,6 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "optional": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3737,27 +2872,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/gaxios": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", @@ -3879,28 +2993,6 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/google-auth-library": { "version": "9.15.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", @@ -4029,13 +3121,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC", - "optional": true - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4049,9 +3134,9 @@ } }, "node_modules/hono": { - "version": "4.12.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", - "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -4088,13 +3173,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause", - "optional": true - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -4115,35 +3193,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -4191,45 +3240,6 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "license": "ISC", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4267,9 +3277,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -4284,23 +3294,6 @@ "node": ">= 0.10" } }, - "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", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "license": "MIT", - "optional": true - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -4520,47 +3513,6 @@ "underscore": "^1.13.1" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", - "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/mammoth": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz", @@ -4621,164 +3573,65 @@ "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-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": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" + "node": ">= 0.6" } }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, + "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": { - "minipass": "^3.0.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, + "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": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" + "dependencies": { + "minipass": "^7.1.2" }, "engines": { - "node": ">=10" + "node": ">= 18" } }, "node_modules/mkdirp-classic": { @@ -4799,16 +3652,6 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-abi": { "version": "3.78.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", @@ -4822,10 +3665,13 @@ } }, "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } }, "node_modules/node-domexception": { "version": "1.0.0", @@ -4868,61 +3714,80 @@ } }, "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", "license": "MIT", "optional": true, "dependencies": { "env-paths": "^2.2.0", - "glob": "^7.1.4", + "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": ">= 10.12.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "license": "ISC", "optional": true, "dependencies": { - "abbrev": "1" + "isexe": "^4.0.0" }, "bin": { - "nopt": "bin/nopt.js" + "node-which": "bin/which.js" }, "engines": { - "node": ">=6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "license": "ISC", "optional": true, "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nth-check": { @@ -5030,22 +3895,6 @@ "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", "license": "BSD-2-Clause" }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -5110,6 +3959,21 @@ "node": ">= 0.8" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -5129,9 +3993,9 @@ } }, "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==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -5259,6 +4123,19 @@ "split2": "^4.1.0" } }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", @@ -5349,33 +4226,22 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "license": "ISC", - "optional": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "license": "MIT", - "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5523,33 +4389,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -5649,13 +4488,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC", - "optional": true - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -5761,13 +4593,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true - }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -5813,47 +4638,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -5870,22 +4654,25 @@ "license": "BSD-3-Clause" }, "node_modules/sqlite3": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", - "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz", + "integrity": "sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "bindings": "^1.5.0", - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "tar": "^6.1.11" + "node-addon-api": "^8.0.0", + "prebuild-install": "^7.1.3", + "tar": "^7.5.10" + }, + "engines": { + "node": ">=20.17.0" }, "optionalDependencies": { - "node-gyp": "8.x" + "node-gyp": "12.x" }, "peerDependencies": { - "node-gyp": "8.x" + "node-gyp": "12.x" }, "peerDependenciesMeta": { "node-gyp": { @@ -5893,19 +4680,6 @@ } } }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -5930,34 +4704,6 @@ "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", - "optional": true, - "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", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -5968,9 +4714,9 @@ } }, "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -5980,20 +4726,19 @@ "license": "MIT" }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "license": "BlueOak-1.0.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { @@ -6030,13 +4775,21 @@ "node": ">=6" } }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "optional": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=8" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/toidentifier": { @@ -6130,15 +4883,15 @@ } }, "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==", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "license": "MIT" }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -6150,26 +4903,6 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "license": "ISC", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, "node_modules/universal-user-agent": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", @@ -6276,16 +5009,6 @@ "node": ">= 8" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6313,6 +5036,21 @@ } } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xmlbuilder": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", @@ -6332,10 +5070,13 @@ } }, "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" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/zod": { "version": "3.25.76", diff --git a/packages/openmemory-js/package.json b/packages/openmemory-js/package.json index 8715ed20..f4b64f07 100644 --- a/packages/openmemory-js/package.json +++ b/packages/openmemory-js/package.json @@ -1,6 +1,6 @@ { "name": "openmemory-js", - "version": "1.3.3", + "version": "1.3.4", "author": "nullure", "bin": { "opm": "./bin/opm.js", @@ -12,13 +12,14 @@ "dev": "nodemon src/server/index.ts", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "build": "tsc -p tsconfig.json", + "test:graphrag": "tsx tests/test_graphrag_bridge.ts", "start": "node dist/server/index.js", "migrate": "tsx src/migrate.ts" }, "dependencies": { - "@aws-sdk/client-bedrock-runtime": "^3.932.0", + "@aws-sdk/client-bedrock-runtime": "^3.1046.0", "@azure/msal-node": "^2.0.0", - "@modelcontextprotocol/sdk": "^1.27.1", + "@modelcontextprotocol/sdk": "^1.29.0", "@notionhq/client": "^2.2.0", "@octokit/rest": "^21.0.0", "cheerio": "^1.0.0", @@ -30,7 +31,7 @@ "openai": "^4.73.0", "pdf-parse": "^2.4.5", "pg": "^8.16.3", - "sqlite3": "^5.1.7", + "sqlite3": "^6.0.1", "turndown": "^7.2.2", "ws": "^8.18.3", "zod": "^3.25.76" diff --git a/packages/openmemory-js/src/ai/mcp.ts b/packages/openmemory-js/src/ai/mcp.ts index 54177401..d7fc1862 100644 --- a/packages/openmemory-js/src/ai/mcp.ts +++ b/packages/openmemory-js/src/ai/mcp.ts @@ -19,6 +19,14 @@ import { update_user_summary } from "../memory/user_summary"; import { insert_fact } from "../temporal_graph/store"; import { query_facts_at_time } from "../temporal_graph/query"; import { ToolRegistry } from "./mcp_tools"; +import { + deleteGraphRagDocument, + getGraphRagStatus, + maybeDeleteGraphRagDocument, + maybeSyncGraphRagDocument, + queryGraphRag, + syncGraphRagDocument, +} from "../graphrag/bridge"; const sec_enum = z.enum([ "episodic", @@ -354,6 +362,19 @@ export const create_mcp_srv = () => { if (type === "contextual" || type === "both") { const res = await add_hsg_memory(content, j(tags || []), metadata, u, proj); results.hsg = { id: res.id, primary_sector: res.primary_sector, sectors: res.sectors }; + maybeSyncGraphRagDocument({ + id: res.id, + content, + metadata: { + ...(metadata || {}), + source: "openmemory:mcp/openmemory_store_project", + openmemory_id: res.id, + primary_sector: res.primary_sector, + tags: tags || [], + }, + user_id: u, + project_id: proj, + }); if (u) { update_user_summary(u).catch((err) => console.error("[MCP] user summary update failed:", err)); } @@ -416,6 +437,19 @@ export const create_mcp_srv = () => { // Add to contextual memory system (HSG) const res = await add_hsg_memory(content, j(tags || []), metadata, u, proj); results.hsg = { id: res.id, primary_sector: res.primary_sector, sectors: res.sectors }; + maybeSyncGraphRagDocument({ + id: res.id, + content, + metadata: { + ...(metadata || {}), + source: "openmemory:mcp/openmemory_store", + openmemory_id: res.id, + primary_sector: res.primary_sector, + tags: tags || [], + }, + user_id: u, + project_id: proj, + }); } if ((type === "factual" || type === "both") && facts) { @@ -474,9 +508,10 @@ export const create_mcp_srv = () => { async ({ id, user_id, project_id }) => { const u = uid(user_id); const proj = uid(project_id); + let mem: mem_row | undefined; if (u || proj) { // Pre-check ownership if user_id/project_id provided - const mem = await q.get_mem.get(id); + mem = await q.get_mem.get(id); if (mem) { if (u && mem.user_id !== u) throw new Error(`Memory ${id} not found for user ${u}`); if (proj && mem.project_id && mem.project_id !== proj && mem.project_id !== 'system_global') { @@ -485,6 +520,8 @@ export const create_mcp_srv = () => { } } + mem = mem || await q.get_mem.get(id); + const success = await delete_memory(id); if (!success) { return { @@ -493,6 +530,10 @@ export const create_mcp_srv = () => { }; } + if (mem) { + maybeDeleteGraphRagDocument({ id: mem.id }); + } + return { content: [{ type: "text", text: `Memory ${id} successfully deleted.` }], }; @@ -633,6 +674,155 @@ export const create_mcp_srv = () => { }; }, ); + + registry.tool( + "openmemory_graphrag_status", + "Check the optional FalkorDB GraphRAG bridge status", + {}, + async () => { + const status = await getGraphRagStatus(); + return { + content: [ + { type: "text", text: JSON.stringify(status, null, 2) }, + ], + }; + }, + ); + + registry.tool( + "openmemory_graphrag_query", + "Query the optional FalkorDB GraphRAG knowledge graph. Use this for relationship-heavy or multi-hop questions when GraphRAG is enabled.", + { + query: z.string().min(1).describe("Question to answer from the GraphRAG graph"), + k: z.number().int().min(1).max(32).default(8).describe("Context item limit"), + user_id: z.string().trim().min(1).optional(), + project_id: z.string().trim().min(1).optional(), + return_context: z.boolean().default(true), + }, + async ({ query, k, user_id, project_id, return_context }) => { + const result = await queryGraphRag({ + query, + k, + user_id: uid(user_id), + project_id: uid(project_id), + return_context, + }); + return { + content: [ + { type: "text", text: JSON.stringify(result, null, 2) }, + ], + }; + }, + ); + + registry.tool( + "openmemory_graphrag_sync", + "Sync one OpenMemory memory or explicit content into the optional FalkorDB GraphRAG graph", + { + id: z.string().min(1).optional().describe("Existing OpenMemory memory id"), + content: z.string().min(1).optional().describe("Explicit content to sync when id is not supplied"), + document_id: z.string().min(1).optional().describe("Explicit GraphRAG document id for content sync"), + metadata: z.record(z.any()).optional(), + user_id: z.string().trim().min(1).optional(), + project_id: z.string().trim().min(1).optional(), + finalize: z.boolean().default(false).describe("Ask the bridge to finalize indexes after this sync"), + }, + async ({ id, content, document_id, metadata, user_id, project_id, finalize }) => { + let sync_id = document_id || id; + let sync_content = content; + let sync_metadata = metadata || {}; + let sync_user = uid(user_id); + let sync_project = uid(project_id); + + if (id && sync_content) { + return { + content: [ + { type: "text", text: "Invalid request: id and content are mutually exclusive; use document_id for explicit content sync." }, + ], + }; + } + + if (id) { + const mem = await q.get_mem.get(id); + if (!mem) { + return { + content: [ + { type: "text", text: `Memory ${id} not found.` }, + ], + }; + } + const mem_user = uid(mem.user_id); + const mem_project = uid(mem.project_id); + if (mem_user && sync_user !== mem_user) { + return { + content: [ + { type: "text", text: "Forbidden: user_id is required and must match the existing memory owner." }, + ], + }; + } + if (mem_project && sync_project !== mem_project) { + return { + content: [ + { type: "text", text: "Forbidden: project_id is required and must match the existing memory project." }, + ], + }; + } + sync_id = mem.id; + sync_content = mem.content; + sync_user = sync_user || mem_user; + sync_project = sync_project || mem_project; + sync_metadata = { + ...p(mem.meta || "{}"), + ...sync_metadata, + openmemory_id: mem.id, + primary_sector: mem.primary_sector, + }; + } + + if (!sync_id || !sync_content) { + throw new Error("Either id or content plus document_id is required."); + } + + const result = await syncGraphRagDocument({ + id: sync_id, + content: sync_content, + metadata: sync_metadata, + user_id: sync_user, + project_id: sync_project, + finalize, + }); + return { + content: [ + { type: "text", text: JSON.stringify(result, null, 2) }, + ], + }; + }, + ); + + registry.tool( + "openmemory_graphrag_delete", + "Delete one GraphRAG document by OpenMemory memory id or explicit document id", + { + id: z.string().min(1).optional().describe("Existing OpenMemory memory id"), + document_id: z.string().min(1).optional().describe("Explicit GraphRAG document id"), + finalize: z.boolean().default(false).describe("Ask the bridge to finalize indexes after this delete"), + }, + async ({ id, document_id, finalize }) => { + const delete_id = document_id || id; + if (!delete_id) { + throw new Error("Either id or document_id is required."); + } + const result = await deleteGraphRagDocument({ + id: delete_id, + finalize, + }); + return { + content: [ + { type: "text", text: JSON.stringify(result, null, 2) }, + ], + }; + }, + ); registry.apply(srv); srv.resource( @@ -660,6 +850,10 @@ export const create_mcp_srv = () => { "openmemory_reinforce", "openmemory_list", "openmemory_get", + "openmemory_graphrag_status", + "openmemory_graphrag_query", + "openmemory_graphrag_sync", + "openmemory_graphrag_delete", ], }; return { diff --git a/packages/openmemory-js/src/ai/mcp_tools.ts b/packages/openmemory-js/src/ai/mcp_tools.ts index 43f527e7..a56b4476 100644 --- a/packages/openmemory-js/src/ai/mcp_tools.ts +++ b/packages/openmemory-js/src/ai/mcp_tools.ts @@ -33,9 +33,9 @@ export class ToolRegistry { srv.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: Array.from(this.tools.values()).map(t => { - const jsonSchema = zodToJsonSchema(t.inputSchema, { + const jsonSchema = zodToJsonSchema(t.inputSchema as any, { target: "jsonSchema2019-09" - }) as Record; + } as any) as Record; if (jsonSchema && typeof jsonSchema === 'object') { diff --git a/packages/openmemory-js/src/core/cfg.ts b/packages/openmemory-js/src/core/cfg.ts index 29f2d979..6e23f052 100644 --- a/packages/openmemory-js/src/core/cfg.ts +++ b/packages/openmemory-js/src/core/cfg.ts @@ -112,4 +112,14 @@ export const env = { summary_layers: num(process.env.OM_SUMMARY_LAYERS, 3), keyword_boost: num(process.env.OM_KEYWORD_BOOST, 2.5), keyword_min_length: num(process.env.OM_KEYWORD_MIN_LENGTH, 3), + graphrag_enabled: bool(process.env.OM_GRAPHRAG_ENABLED), + graphrag_url: str(process.env.OM_GRAPHRAG_URL, "http://127.0.0.1:8765"), + graphrag_timeout_ms: num(process.env.OM_GRAPHRAG_TIMEOUT_MS, 120000), + graphrag_write_enabled: bool(process.env.OM_GRAPHRAG_WRITE_ENABLED), + graphrag_allow_unauth_write: bool(process.env.OM_GRAPHRAG_ALLOW_UNAUTH_WRITE), + graphrag_allow_global_query: bool(process.env.OM_GRAPHRAG_ALLOW_GLOBAL_QUERY), + graphrag_allow_unfiltered_scoped_query: bool(process.env.OM_GRAPHRAG_ALLOW_UNFILTERED_SCOPED_QUERY), + graphrag_bridge_api_key: process.env.OM_GRAPHRAG_BRIDGE_API_KEY || "", + graphrag_sync_on_add: bool(process.env.OM_GRAPHRAG_SYNC_ON_ADD), + graphrag_context_enabled: bool(process.env.OM_GRAPHRAG_CONTEXT_ENABLED), }; diff --git a/packages/openmemory-js/src/core/db.ts b/packages/openmemory-js/src/core/db.ts index e698a2c5..5e9e7940 100644 --- a/packages/openmemory-js/src/core/db.ts +++ b/packages/openmemory-js/src/core/db.ts @@ -469,6 +469,17 @@ if (is_pg) { const db = new sqlite3.Database(db_path); const sqlite_vector_table = process.env.OM_VECTOR_TABLE || "vectors"; + const add_column_if_missing = (sql: string) => { + db.run(sql, (err) => { + if ( + err && + !err.message.includes("duplicate column") && + !err.message.includes("no such table") + ) { + console.error(`[DB] Schema repair failed: ${err.message}`); + } + }); + }; db.serialize(() => { db.run("PRAGMA journal_mode=WAL"); db.run("PRAGMA synchronous=NORMAL"); @@ -500,6 +511,14 @@ if (is_pg) { db.run( `create table if not exists temporal_facts(id text primary key,user_id text,project_id text,subject text not null,predicate text not null,object text not null,valid_from integer not null,valid_to integer,confidence real not null check(confidence >= 0 and confidence <= 1),last_updated integer not null,metadata text,unique(subject,predicate,object,valid_from))`, ); + add_column_if_missing("alter table memories add column user_id text"); + add_column_if_missing("alter table memories add column project_id text"); + add_column_if_missing(`alter table ${sqlite_vector_table} add column user_id text`); + add_column_if_missing(`alter table ${sqlite_vector_table} add column project_id text`); + add_column_if_missing("alter table waypoints add column user_id text"); + add_column_if_missing("alter table waypoints add column project_id text"); + add_column_if_missing("alter table temporal_facts add column user_id text"); + add_column_if_missing("alter table temporal_facts add column project_id text"); db.run( "create index if not exists idx_temporal_user on temporal_facts(user_id)", ); diff --git a/packages/openmemory-js/src/core/migrate.ts b/packages/openmemory-js/src/core/migrate.ts index 5ed3149f..34d113c8 100644 --- a/packages/openmemory-js/src/core/migrate.ts +++ b/packages/openmemory-js/src/core/migrate.ts @@ -151,14 +151,15 @@ async function run_sqlite_migration( ): Promise { log(`Running migration: ${m.version} - ${m.desc}`); - const has_user_id = await check_column_exists_sqlite( + const guard_column = m.version >= "1.3.0" ? "project_id" : "user_id"; + const has_guard_column = await check_column_exists_sqlite( db, "memories", - "user_id", + guard_column, ); - if (has_user_id) { + if (has_guard_column) { log( - `Migration ${m.version} already applied (user_id exists), skipping`, + `Migration ${m.version} already applied (${guard_column} exists), skipping`, ); await set_db_version_sqlite(db, m.version); return; @@ -237,11 +238,12 @@ async function run_pg_migration(pool: Pool, m: Migration): Promise { const sc = process.env.OM_PG_SCHEMA || "public"; const mt = process.env.OM_PG_TABLE || "openmemory_memories"; - const has_user_id = await check_column_exists_pg(pool, mt, "user_id"); + const guard_column = m.version >= "1.3.0" ? "project_id" : "user_id"; + const has_guard_column = await check_column_exists_pg(pool, mt, guard_column); - if (has_user_id) { + if (has_guard_column) { log( - `Migration ${m.version} already applied (user_id exists), skipping`, + `Migration ${m.version} already applied (${guard_column} exists), skipping`, ); await set_db_version_pg(pool, m.version); return; diff --git a/packages/openmemory-js/src/core/models.ts b/packages/openmemory-js/src/core/models.ts index 0e2cf6b7..6192ee6b 100644 --- a/packages/openmemory-js/src/core/models.ts +++ b/packages/openmemory-js/src/core/models.ts @@ -7,21 +7,27 @@ let cfg: model_cfg | null = null; export const load_models = (): model_cfg => { if (cfg) return cfg; - const p = join(__dirname, "../../../models.yml"); - if (!existsSync(p)) { + const candidates = [ + join(__dirname, "../../models.yml"), + join(__dirname, "../../../models.yml"), + ]; + const p = candidates.find((candidate) => existsSync(candidate)); + if (!p) { console.error("[MODELS] models.yml not found, using defaults"); - return get_defaults(); + cfg = get_defaults(); + return cfg; } try { const yml = readFileSync(p, "utf-8"); cfg = parse_yaml(yml); console.error( - `[MODELS] Loaded models.yml (${Object.keys(cfg).length} sectors)`, + `[MODELS] Loaded models.yml from ${p} (${Object.keys(cfg).length} sectors)`, ); return cfg; } catch (e) { console.error("[MODELS] Failed to parse models.yml:", e); - return get_defaults(); + cfg = get_defaults(); + return cfg; } }; diff --git a/packages/openmemory-js/src/graphrag/bridge.ts b/packages/openmemory-js/src/graphrag/bridge.ts new file mode 100644 index 00000000..e2c4b270 --- /dev/null +++ b/packages/openmemory-js/src/graphrag/bridge.ts @@ -0,0 +1,215 @@ +import { env } from "../core/cfg"; + +export type GraphRagDocument = { + id: string; + content: string; + metadata?: Record; + user_id?: string | null; + project_id?: string | null; + finalize?: boolean; +}; + +export type GraphRagQuery = { + query: string; + k?: number; + user_id?: string | null; + project_id?: string | null; + return_context?: boolean; +}; + +export type GraphRagDelete = { + id: string; + finalize?: boolean; +}; + +export type GraphRagBridgeResult = { + enabled: boolean; + ok: boolean; + skipped?: boolean; + reason?: string; + data?: unknown; + error?: string; +}; + +const baseUrl = () => env.graphrag_url.replace(/\/+$/, ""); + +const disabled = (reason = "OM_GRAPHRAG_ENABLED is not true"): GraphRagBridgeResult => ({ + enabled: false, + ok: false, + skipped: true, + reason, +}); + +const writeDisabled = (): GraphRagBridgeResult => ({ + enabled: env.graphrag_enabled, + ok: false, + skipped: true, + reason: "OM_GRAPHRAG_WRITE_ENABLED is not true", +}); + +const unauthWriteDisabled = (): GraphRagBridgeResult => ({ + enabled: env.graphrag_enabled, + ok: false, + skipped: true, + reason: "OM_API_KEY is required for GraphRAG writes unless OM_GRAPHRAG_ALLOW_UNAUTH_WRITE is true", +}); + +const globalQueryDisabled = (): GraphRagBridgeResult => ({ + enabled: env.graphrag_enabled, + ok: false, + skipped: true, + reason: "Global GraphRAG queries are disabled until OM_GRAPHRAG_ALLOW_GLOBAL_QUERY is true", +}); + +const canWrite = (): GraphRagBridgeResult | null => { + if (!env.graphrag_write_enabled) return writeDisabled(); + if (!env.api_key && !env.graphrag_allow_unauth_write) return unauthWriteDisabled(); + return null; +}; + +function bridgeSaysNotOk(data: unknown): GraphRagBridgeResult | null { + if (!data || typeof data !== "object") return null; + const payload = data as { ok?: unknown; error?: unknown; detail?: unknown }; + if (payload.ok !== false) return null; + const error = payload.error || payload.detail || "GraphRAG bridge returned ok=false"; + return { enabled: true, ok: false, error: String(error), data }; +} + +async function requestBridge(path: string, init?: RequestInit): Promise { + if (!env.graphrag_enabled) return disabled(); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), env.graphrag_timeout_ms); + + try { + const response = await fetch(`${baseUrl()}${path}`, { + ...init, + signal: controller.signal, + headers: { + "content-type": "application/json", + ...(env.graphrag_bridge_api_key ? { "x-graph-api-key": env.graphrag_bridge_api_key } : {}), + ...(init?.headers || {}), + }, + }); + + const text = await response.text(); + let data: unknown = null; + if (text) { + try { + data = JSON.parse(text); + } catch { + data = { text }; + } + } + + if (!response.ok) { + return { + enabled: true, + ok: false, + error: `GraphRAG bridge HTTP ${response.status}`, + data, + }; + } + + const bridgeFailure = bridgeSaysNotOk(data); + if (bridgeFailure) return bridgeFailure; + + return { enabled: true, ok: true, data }; + } catch (error: any) { + return { + enabled: true, + ok: false, + error: error?.name === "AbortError" ? "GraphRAG bridge timeout" : String(error?.message || error), + }; + } finally { + clearTimeout(timeout); + } +} + +export async function getGraphRagStatus(): Promise { + return requestBridge("/health", { method: "GET" }); +} + +export async function queryGraphRag(query: GraphRagQuery): Promise { + if (!query.query?.trim()) { + return { enabled: env.graphrag_enabled, ok: false, error: "query is required" }; + } + const scoped = Boolean(query.user_id || query.project_id); + if (!scoped && !env.graphrag_allow_global_query) return globalQueryDisabled(); + + return requestBridge("/query", { + method: "POST", + body: JSON.stringify({ + query: query.query, + k: query.k, + user_id: query.user_id || undefined, + project_id: query.project_id || undefined, + return_context: query.return_context ?? true, + }), + }); +} + +export async function syncGraphRagDocument(doc: GraphRagDocument): Promise { + const writeGate = canWrite(); + if (writeGate) return writeGate; + + if (!doc.id?.trim()) { + return { enabled: env.graphrag_enabled, ok: false, error: "document id is required" }; + } + if (!doc.content?.trim()) { + return { enabled: env.graphrag_enabled, ok: false, error: "document content is required" }; + } + + return requestBridge("/documents/upsert", { + method: "POST", + body: JSON.stringify({ + document_id: doc.id, + content: doc.content, + metadata: doc.metadata || {}, + user_id: doc.user_id || undefined, + project_id: doc.project_id || undefined, + finalize: doc.finalize ?? false, + }), + }); +} + +export async function deleteGraphRagDocument(doc: GraphRagDelete): Promise { + const writeGate = canWrite(); + if (writeGate) return writeGate; + + if (!doc.id?.trim()) { + return { enabled: env.graphrag_enabled, ok: false, error: "document id is required" }; + } + + return requestBridge("/documents/delete", { + method: "POST", + body: JSON.stringify({ + document_id: doc.id, + finalize: doc.finalize ?? false, + }), + }); +} + +export function maybeSyncGraphRagDocument(doc: GraphRagDocument): void { + if (!env.graphrag_enabled || !env.graphrag_sync_on_add || canWrite()) return; + + syncGraphRagDocument(doc).then((result) => { + if (!result.ok) { + console.error("[GraphRAG] sync failed:", result.error || result.reason || result.data); + } + }).catch((error) => { + console.error("[GraphRAG] sync failed:", error); + }); +} + +export function maybeDeleteGraphRagDocument(doc: GraphRagDelete): void { + if (!env.graphrag_enabled || !env.graphrag_sync_on_add || canWrite()) return; + + deleteGraphRagDocument(doc).then((result) => { + if (!result.ok) { + console.error("[GraphRAG] delete failed:", result.error || result.reason || result.data); + } + }).catch((error) => { + console.error("[GraphRAG] delete failed:", error); + }); +} diff --git a/packages/openmemory-js/src/server/routes/graphrag.ts b/packages/openmemory-js/src/server/routes/graphrag.ts new file mode 100644 index 00000000..a1c28983 --- /dev/null +++ b/packages/openmemory-js/src/server/routes/graphrag.ts @@ -0,0 +1,113 @@ +import { q } from "../../core/db"; +import { env } from "../../core/cfg"; +import { + deleteGraphRagDocument, + getGraphRagStatus, + queryGraphRag, + syncGraphRagDocument, +} from "../../graphrag/bridge"; +import { p } from "../../utils"; + +export function graphrag(app: any) { + app.get("/graphrag/status", async (_req: any, res: any) => { + const status = await getGraphRagStatus(); + res.status(status.ok || status.skipped ? 200 : 503).json({ + ...status, + config: { + enabled: env.graphrag_enabled, + bridge_url: env.graphrag_url, + write_enabled: env.graphrag_write_enabled, + allow_unauth_write: env.graphrag_allow_unauth_write, + allow_global_query: env.graphrag_allow_global_query, + allow_unfiltered_scoped_query: env.graphrag_allow_unfiltered_scoped_query, + bridge_auth_configured: Boolean(env.graphrag_bridge_api_key), + sync_on_add: env.graphrag_sync_on_add, + ide_context_enabled: env.graphrag_context_enabled, + }, + }); + }); + + app.post("/graphrag/query", async (req: any, res: any) => { + const result = await queryGraphRag({ + query: req.body?.query, + k: req.body?.k || req.body?.limit, + user_id: req.body?.user_id, + project_id: req.body?.project_id, + return_context: req.body?.return_context, + }); + res.status(result.ok || result.skipped ? 200 : 502).json(result); + }); + + app.post("/graphrag/sync", async (req: any, res: any) => { + const id = req.body?.id || req.body?.memory_id; + let content = req.body?.content; + let metadata = req.body?.metadata || {}; + let user_id = req.body?.user_id; + let project_id = req.body?.project_id; + + if (id && content) { + return res.status(400).json({ + ok: false, + error: "invalid_request", + message: "id and content are mutually exclusive; use document_id for explicit content sync", + }); + } + + if (id) { + const mem = await q.get_mem.get(id); + if (!mem) { + return res.status(404).json({ ok: false, error: `memory ${id} not found` }); + } + if (mem.user_id && user_id !== mem.user_id) { + return res.status(403).json({ + ok: false, + error: "forbidden", + message: "user_id is required and must match the existing memory owner", + }); + } + if (mem.project_id && project_id !== mem.project_id) { + return res.status(403).json({ + ok: false, + error: "forbidden", + message: "project_id is required and must match the existing memory project", + }); + } + content = mem.content; + metadata = { + ...p(mem.meta || "{}"), + ...metadata, + openmemory_id: mem.id, + primary_sector: mem.primary_sector, + }; + user_id = user_id || mem.user_id; + project_id = project_id || mem.project_id; + } + + const result = await syncGraphRagDocument({ + id: id || req.body?.document_id, + content, + metadata, + user_id, + project_id, + finalize: req.body?.finalize, + }); + res.status(result.ok || result.skipped ? 200 : 502).json(result); + }); + + app.post("/graphrag/delete", async (req: any, res: any) => { + const document_id = req.body?.document_id || req.body?.id || req.body?.memory_id; + if (!document_id) { + return res.status(400).json({ + ok: false, + error: "invalid_request", + message: "document_id or id is required", + }); + } + + const result = await deleteGraphRagDocument({ + id: document_id, + finalize: req.body?.finalize, + }); + res.status(result.ok || result.skipped ? 200 : 502).json(result); + }); +} diff --git a/packages/openmemory-js/src/server/routes/ide.ts b/packages/openmemory-js/src/server/routes/ide.ts index 09fd27c5..3df3f155 100644 --- a/packages/openmemory-js/src/server/routes/ide.ts +++ b/packages/openmemory-js/src/server/routes/ide.ts @@ -3,6 +3,8 @@ import { add_hsg_memory, hsg_query } from "../../memory/hsg"; import { update_user_summary } from "../../memory/user_summary"; import { j, p } from "../../utils"; import * as crypto from "crypto"; +import { env } from "../../core/cfg"; +import { maybeSyncGraphRagDocument, queryGraphRag } from "../../graphrag/bridge"; export function ide(app: any) { app.post("/api/ide/events", async (req: any, res: any) => { try { @@ -12,6 +14,7 @@ export function ide(app: any) { const session_id = req.body.session_id || "default"; const metadata = req.body.metadata || {}; const user_id = req.body.user_id || "anonymous"; + const project_id = req.body.project_id || metadata.project_id; if (!event_type) return res.status(400).json({ err: "event_type_required" }); @@ -45,8 +48,22 @@ export function ide(app: any) { undefined, full_metadata, user_id, + project_id, ); + maybeSyncGraphRagDocument({ + id: result.id, + content: memory_content, + metadata: { + ...full_metadata, + source: "openmemory:/api/ide/events", + openmemory_id: result.id, + primary_sector: result.primary_sector, + }, + user_id, + project_id, + }); + if (user_id && user_id !== "anonymous") { update_user_summary(user_id).catch(err => @@ -72,10 +89,24 @@ export function ide(app: any) { const k = req.body.k || req.body.limit || 5; const session_id = req.body.session_id; const file_path = req.body.file_path; + const user_id = req.body.user_id; + const project_id = req.body.project_id; if (!query) return res.status(400).json({ err: "query_required" }); - const results = await hsg_query(query, k); + const results = await hsg_query(query, k, { + ...(user_id ? { user_id } : {}), + ...(project_id ? { project_id } : {}), + }); + const graph_context = env.graphrag_context_enabled + ? await queryGraphRag({ + query, + k, + user_id, + project_id, + return_context: true, + }) + : undefined; let filtered = results; @@ -112,6 +143,7 @@ export function ide(app: any) { res.json({ success: true, memories: formatted, + graphrag: graph_context, total: formatted.length, query: query, }); @@ -126,6 +158,7 @@ export function ide(app: any) { const user_id = req.body.user_id || "anonymous"; const project_name = req.body.project_name || "unknown"; const ide_name = req.body.ide_name || "unknown"; + const project_id = req.body.project_id; const session_id = `session_${Date.now()}_${crypto.randomBytes(7).toString("hex")}`; const now_ts = Date.now(); @@ -136,13 +169,27 @@ export function ide(app: any) { ide_session_id: session_id, ide_user_id: user_id, ide_project_name: project_name, + project_id: project_id, ide_name: ide_name, session_start_time: now_ts, session_type: "ide_session", ide_mode: true, }; - const result = await add_hsg_memory(content, undefined, metadata, user_id); + const result = await add_hsg_memory(content, undefined, metadata, user_id, project_id); + + maybeSyncGraphRagDocument({ + id: result.id, + content, + metadata: { + ...metadata, + source: "openmemory:/api/ide/session/start", + openmemory_id: result.id, + primary_sector: result.primary_sector, + }, + user_id, + project_id, + }); if (user_id && user_id !== "anonymous") { update_user_summary(user_id).catch(err => @@ -169,6 +216,7 @@ export function ide(app: any) { try { const session_id = req.body.session_id; const user_id = req.body.user_id || "anonymous"; + const project_id = req.body.project_id; if (!session_id) return res.status(400).json({ err: "session_id_required" }); @@ -216,7 +264,20 @@ export function ide(app: any) { ide_mode: true, }; - const result = await add_hsg_memory(summary, undefined, metadata, user_id); + const result = await add_hsg_memory(summary, undefined, metadata, user_id, project_id); + + maybeSyncGraphRagDocument({ + id: result.id, + content: summary, + metadata: { + ...metadata, + source: "openmemory:/api/ide/session/end", + openmemory_id: result.id, + primary_sector: result.primary_sector, + }, + user_id, + project_id, + }); if (user_id && user_id !== "anonymous") { update_user_summary(user_id).catch(err => diff --git a/packages/openmemory-js/src/server/routes/index.ts b/packages/openmemory-js/src/server/routes/index.ts index 7e3765b0..fbab61bc 100644 --- a/packages/openmemory-js/src/server/routes/index.ts +++ b/packages/openmemory-js/src/server/routes/index.ts @@ -9,6 +9,7 @@ import { temporal } from "./temporal"; import { dash } from "./dashboard"; import { vercel } from "./vercel"; import { src } from "./sources"; +import { graphrag } from "./graphrag"; export function routes(app: any) { sys(app); @@ -22,5 +23,6 @@ export function routes(app: any) { dash(app); vercel(app); src(app); + graphrag(app); } diff --git a/packages/openmemory-js/src/server/routes/memory.ts b/packages/openmemory-js/src/server/routes/memory.ts index 29c16450..ffdd2731 100644 --- a/packages/openmemory-js/src/server/routes/memory.ts +++ b/packages/openmemory-js/src/server/routes/memory.ts @@ -15,6 +15,10 @@ import type { ingest_req, ingest_url_req, } from "../../core/types"; +import { + maybeDeleteGraphRagDocument, + maybeSyncGraphRagDocument, +} from "../../graphrag/bridge"; export function mem(app: any) { app.post("/memory/add", async (req: any, res: any) => { @@ -26,9 +30,24 @@ export function mem(app: any) { j(b.tags || []), b.metadata, b.user_id, + b.project_id, ); res.json(m); + maybeSyncGraphRagDocument({ + id: m.id, + content: b.content, + metadata: { + ...(b.metadata || {}), + openmemory_id: m.id, + source: "openmemory:/memory/add", + primary_sector: m.primary_sector, + tags: b.tags || [], + }, + user_id: b.user_id, + project_id: b.project_id, + }); + if (b.user_id) { update_user_summary(b.user_id).catch((e) => console.error("[mem] user summary update failed:", e), @@ -76,6 +95,7 @@ export function mem(app: any) { sectors: b.filters?.sector ? [b.filters.sector] : undefined, minSalience: b.filters?.min_score, user_id: b.filters?.user_id || b.user_id, + project_id: b.filters?.project_id || b.project_id, startTime: b.filters?.startTime ?? b.startTime, endTime: b.filters?.endTime ?? b.endTime, }; @@ -129,6 +149,24 @@ export function mem(app: any) { } const r = await update_memory(id, b.content, b.tags, b.metadata); + if (b.content !== undefined && b.content !== m.content) { + const updated_mem = await q.get_mem.get(id); + if (updated_mem) { + maybeSyncGraphRagDocument({ + id: updated_mem.id, + content: updated_mem.content, + metadata: { + ...p(updated_mem.meta || "{}"), + openmemory_id: updated_mem.id, + source: "openmemory:/memory/patch", + primary_sector: updated_mem.primary_sector, + tags: p(updated_mem.tags || "[]"), + }, + user_id: updated_mem.user_id, + project_id: updated_mem.project_id, + }); + } + } res.json(r); } catch (e: any) { if (e.message.includes("not found")) { @@ -227,6 +265,7 @@ export function mem(app: any) { await q.del_mem.run(id); await vector_store.deleteVectors(id); await q.del_waypoints.run(id, id); + maybeDeleteGraphRagDocument({ id: m.id }); res.json({ ok: true }); } catch (e: any) { res.status(500).json({ err: "internal" }); diff --git a/packages/openmemory-js/tests/test_graphrag_bridge.ts b/packages/openmemory-js/tests/test_graphrag_bridge.ts new file mode 100644 index 00000000..ea0f823d --- /dev/null +++ b/packages/openmemory-js/tests/test_graphrag_bridge.ts @@ -0,0 +1,128 @@ +import assert from "assert"; +import http from "http"; + +async function readBody(req: http.IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks).toString("utf8"); +} + +async function main() { + let syncRequests = 0; + let deleteRequests = 0; + let queryRequests = 0; + let healthOk = true; + + const server = http.createServer(async (req, res) => { + res.setHeader("content-type", "application/json"); + + if (req.method === "GET" && req.url === "/health") { + res.end(JSON.stringify({ ok: healthOk, source: "test-bridge", error: healthOk ? null : "not configured" })); + return; + } + + if (req.method === "POST" && req.url === "/query") { + queryRequests += 1; + const body = JSON.parse(await readBody(req)); + res.end(JSON.stringify({ ok: true, query: body.query })); + return; + } + + if (req.method === "POST" && req.url === "/documents/upsert") { + syncRequests += 1; + res.end(JSON.stringify({ ok: true })); + return; + } + + if (req.method === "POST" && req.url === "/documents/delete") { + deleteRequests += 1; + res.end(JSON.stringify({ ok: true })); + return; + } + + res.statusCode = 404; + res.end(JSON.stringify({ ok: false, error: "not found" })); + }); + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + assert(address && typeof address === "object", "test server did not expose an address"); + + process.env.OM_TIER = "hybrid"; + process.env.OM_GRAPHRAG_ENABLED = "true"; + process.env.OM_GRAPHRAG_URL = `http://127.0.0.1:${address.port}`; + process.env.OM_GRAPHRAG_TIMEOUT_MS = "3000"; + process.env.OM_GRAPHRAG_BRIDGE_API_KEY = "test-bridge-key"; + process.env.OM_GRAPHRAG_WRITE_ENABLED = "true"; + process.env.OM_GRAPHRAG_ALLOW_UNAUTH_WRITE = "false"; + process.env.OM_GRAPHRAG_ALLOW_GLOBAL_QUERY = "true"; + process.env.OM_GRAPHRAG_ALLOW_UNFILTERED_SCOPED_QUERY = "false"; + process.env.OM_GRAPHRAG_SYNC_ON_ADD = "true"; + process.env.OM_GRAPHRAG_CONTEXT_ENABLED = "true"; + delete process.env.OM_API_KEY; + + try { + const bridge = await import("../src/graphrag/bridge"); + + const status = await bridge.getGraphRagStatus(); + assert.strictEqual(status.ok, true, "status should call the bridge when GraphRAG is enabled"); + + healthOk = false; + const failingStatus = await bridge.getGraphRagStatus(); + assert.strictEqual(failingStatus.ok, false, "bridge health ok=false should not be treated as success"); + assert.strictEqual(failingStatus.error, "not configured"); + healthOk = true; + + const query = await bridge.queryGraphRag({ query: "relationship-heavy question" }); + assert.strictEqual(query.ok, true, "query should call the bridge when GraphRAG is enabled"); + assert.strictEqual(queryRequests, 1, "query should issue one bridge request"); + + const scopedQuery = await bridge.queryGraphRag({ + query: "relationship-heavy scoped question", + project_id: "project-alpha", + }); + assert.strictEqual(scopedQuery.ok, true, "scoped queries should reach the bridge now that server-side filtering exists"); + assert.strictEqual(queryRequests, 2, "scoped query should issue a bridge request"); + + const sync = await bridge.syncGraphRagDocument({ + id: "memory-1", + content: "private memory content", + }); + assert.strictEqual(sync.skipped, true, "sync should be skipped without API key or explicit unauth write override"); + assert.strictEqual( + sync.reason, + "OM_API_KEY is required for GraphRAG writes unless OM_GRAPHRAG_ALLOW_UNAUTH_WRITE is true", + ); + assert.strictEqual(syncRequests, 0, "write-gated sync must not call /documents/upsert"); + + bridge.maybeSyncGraphRagDocument({ + id: "memory-2", + content: "private memory content", + }); + await new Promise((resolve) => setTimeout(resolve, 100)); + assert.strictEqual(syncRequests, 0, "auto-sync must not call /documents/upsert without authenticated write gate"); + + const deletion = await bridge.deleteGraphRagDocument({ id: "memory-1" }); + assert.strictEqual(deletion.skipped, true, "delete should be skipped without API key or explicit unauth write override"); + assert.strictEqual( + deletion.reason, + "OM_API_KEY is required for GraphRAG writes unless OM_GRAPHRAG_ALLOW_UNAUTH_WRITE is true", + ); + assert.strictEqual(deleteRequests, 0, "write-gated delete must not call /documents/delete"); + + bridge.maybeDeleteGraphRagDocument({ id: "memory-2" }); + await new Promise((resolve) => setTimeout(resolve, 100)); + assert.strictEqual(deleteRequests, 0, "auto-delete must not call /documents/delete without authenticated write gate"); + + console.log("GraphRAG bridge authenticated write gate verified."); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/start-openmemory.ps1 b/start-openmemory.ps1 index c5c3e716..64f843d1 100644 --- a/start-openmemory.ps1 +++ b/start-openmemory.ps1 @@ -11,7 +11,7 @@ $Stderr = Join-Path $LogDir "openmemory-server.err.log" function Test-OpenMemoryHealth { try { - $response = Invoke-RestMethod -Uri "http://127.0.0.1:8080/health" -TimeoutSec 3 + $response = Invoke-RestMethod -Uri "http://127.0.0.1:8180/health" -TimeoutSec 3 return [bool]$response.ok } catch { return $false @@ -28,8 +28,8 @@ if (-not (Test-Path -LiteralPath $Opm)) { New-Item -ItemType Directory -Force -Path $DataDir, $LogDir | Out-Null -$env:OPENMEMORY_URL = "http://127.0.0.1:8080" -$env:OM_PORT = "8080" +$env:OPENMEMORY_URL = "http://127.0.0.1:8180" +$env:OM_PORT = "8180" $env:OM_DB_PATH = $DbPath $env:OM_TIER = "hybrid" $env:NO_COLOR = "1" @@ -50,4 +50,4 @@ while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 } -throw "OpenMemory did not become healthy on http://127.0.0.1:8080/health. Check $Stdout and $Stderr." +throw "OpenMemory did not become healthy on http://127.0.0.1:8180/health. Check $Stdout and $Stderr." diff --git a/tools/openmemory-graphrag-bridge/Dockerfile b/tools/openmemory-graphrag-bridge/Dockerfile new file mode 100644 index 00000000..7912c980 --- /dev/null +++ b/tools/openmemory-graphrag-bridge/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +WORKDIR /app +COPY requirements.txt . +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cpu torch==2.12.0+cpu \ + && pip install --no-cache-dir -r requirements.txt \ + && apt-get purge -y --auto-remove build-essential \ + && rm -rf /var/lib/apt/lists/* +COPY server.py . + +EXPOSE 8765 +CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8765"] diff --git a/tools/openmemory-graphrag-bridge/README.md b/tools/openmemory-graphrag-bridge/README.md new file mode 100644 index 00000000..fe525d64 --- /dev/null +++ b/tools/openmemory-graphrag-bridge/README.md @@ -0,0 +1,116 @@ +# OpenMemory GraphRAG Bridge + +Optional HTTP sidecar that lets the Node OpenMemory runtime use FalkorDB +GraphRAG-SDK without replacing the existing HSG/temporal memory path. + +## Run locally + +```bash +python -m venv .venv +.venv/Scripts/pip install -r requirements.txt +set FALKORDB_HOST=127.0.0.1 +set FALKORDB_PORT=6379 +set OM_GRAPHRAG_BRIDGE_API_KEY= +set OM_GRAPHRAG_GRAPH_NAME=openmemory +set OM_GRAPHRAG_LLM_MODEL= +set OM_GRAPHRAG_LLM_MAX_TOKENS= +set OM_GRAPHRAG_EMBEDDER_MODEL= +set OM_GRAPHRAG_EMBEDDER_DIMENSIONS= +set OLLAMA_API_BASE=http://127.0.0.1:11434 +set OPENAI_API_KEY=... +.venv/Scripts/uvicorn server:app --host 127.0.0.1 --port 8765 +``` + +Start FalkorDB separately: + +```bash +docker run -d -p 127.0.0.1:6379:6379 -p 127.0.0.1:3001:3000 --name falkordb falkordb/falkordb:latest +``` + +## OpenMemory flags + +```bash +OM_GRAPHRAG_ENABLED=true +OM_GRAPHRAG_URL=http://127.0.0.1:8765 +OM_GRAPHRAG_BRIDGE_API_KEY= +OM_GRAPHRAG_WRITE_ENABLED=false +OM_GRAPHRAG_ALLOW_UNAUTH_WRITE=false +OM_GRAPHRAG_ALLOW_GLOBAL_QUERY=false +OM_GRAPHRAG_SYNC_ON_ADD=false +OM_GRAPHRAG_CONTEXT_ENABLED=false +OM_GRAPHRAG_EMBEDDER_DIMENSIONS= +OM_GRAPHRAG_ENABLE_LEGACY_SCOPE_RECOVERY=false +``` + +Keep `OM_GRAPHRAG_WRITE_ENABLED=false` and `OM_GRAPHRAG_SYNC_ON_ADD=false` +until the source allowlist/redaction policy for writes is explicit. The bridge +does not default to an external LLM/embedder; choose model env vars deliberately. +When enabling writes through OpenMemory, prefer a non-empty `OM_API_KEY` over +`OM_GRAPHRAG_ALLOW_UNAUTH_WRITE=true`. +For local Ollama models, set `OLLAMA_API_BASE` and make the GraphRAG embedding +dimension match the model output. Example: `ollama/bge-m3:latest` uses `1024`. +If local Ollama verify/relationship extraction is too slow on cold starts, set +`OM_GRAPHRAG_LLM_MAX_TOKENS` to cap the bridge LLM completion path. +`OM_GRAPHRAG_GLINER_ALLOW_ONLINE_FALLBACK` is now an operator-only switch: +keep it `false` for deterministic cached startup and enable it only when you +explicitly want startup-time HF fallback on cache miss or cache-load failure. +`OM_GRAPHRAG_ALLOW_UNFILTERED_SCOPED_QUERY` is now a legacy compatibility flag +for stale graphs; ordinary scoped queries no longer require it after scope +metadata backfill. +`OM_GRAPHRAG_ENABLE_LEGACY_SCOPE_RECOVERY` is an operator-only recovery switch: +keep it `false` for a healthy backfilled graph and enable it only if you must +serve a stale corpus before running `/scope/backfill`. + +`GET /health` reports the effective storage contract in +`scope_storage_contract`. It now also exposes +`scope_operator_recovery_path_present=true` to mark the dormant operator-only +recovery path. `scope_storage_compatibility_path_present` is kept as a +deprecated alias for older local consumers. + +## Operator helpers + +Run focused bridge logic tests inside the bridge dependency image: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\run-tests.ps1 +``` + +Optionally rebuild the image first: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\run-tests.ps1 -RebuildImage +``` + +Run a hermetic FalkorDB + bridge end-to-end smoke on the host. By default this +uses a local fake Ollama-compatible stub so it does not depend on host Ollama +or live Hugging Face downloads: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\run-e2e.ps1 +``` + +Optionally rebuild the image first: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\run-e2e.ps1 -RebuildImage +``` + +For an operator-realistic run against the host Ollama runtime instead of the +hermetic stub: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\run-e2e.ps1 -UseHostOllama +``` + +Backfill structured scope metadata onto older `Document`/`Chunk` nodes: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\backfill-scope.ps1 -DryRun +powershell -NoProfile -ExecutionPolicy Bypass -File .\backfill-scope.ps1 +``` + +Target specific document ids when needed: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\backfill-scope.ps1 -DocumentIds doc-1,doc-2 +``` diff --git a/tools/openmemory-graphrag-bridge/backfill-scope.ps1 b/tools/openmemory-graphrag-bridge/backfill-scope.ps1 new file mode 100644 index 00000000..b3b81d0a --- /dev/null +++ b/tools/openmemory-graphrag-bridge/backfill-scope.ps1 @@ -0,0 +1,37 @@ +param( + [string[]]$DocumentIds, + [int]$Limit = 1000, + [switch]$DryRun, + [string]$BridgeUrl = 'http://127.0.0.1:8765', + [string]$BridgeApiKey +) + +$ErrorActionPreference = 'Stop' + +if (-not $BridgeApiKey) { + $BridgeApiKey = docker inspect openmemory-graphrag-bridge-1 --format '{{range .Config.Env}}{{println .}}{{end}}' ` + | Select-String '^OM_GRAPHRAG_BRIDGE_API_KEY=' ` + | ForEach-Object { ($_ -split '=', 2)[1] } ` + | Select-Object -First 1 +} + +if (-not $BridgeApiKey) { + throw 'OM_GRAPHRAG_BRIDGE_API_KEY not found. Pass -BridgeApiKey explicitly or start openmemory-graphrag-bridge-1 first.' +} + +$headers = @{ + 'x-graph-api-key' = $BridgeApiKey + 'Content-Type' = 'application/json' +} + +$payload = @{ + dry_run = [bool]$DryRun + limit = $Limit + document_ids = @($DocumentIds | Where-Object { $_ -and $_.Trim() }) +} + +Invoke-RestMethod ` + -Uri ($BridgeUrl.TrimEnd('/') + '/scope/backfill') ` + -Method Post ` + -Headers $headers ` + -Body ($payload | ConvertTo-Json -Depth 6) diff --git a/tools/openmemory-graphrag-bridge/requirements.txt b/tools/openmemory-graphrag-bridge/requirements.txt new file mode 100644 index 00000000..084bfee0 --- /dev/null +++ b/tools/openmemory-graphrag-bridge/requirements.txt @@ -0,0 +1,3 @@ +fastapi>=0.115,<1 +uvicorn[standard]>=0.32,<1 +graphrag-sdk[litellm]==1.1.1 diff --git a/tools/openmemory-graphrag-bridge/run-e2e.ps1 b/tools/openmemory-graphrag-bridge/run-e2e.ps1 new file mode 100644 index 00000000..75fdde89 --- /dev/null +++ b/tools/openmemory-graphrag-bridge/run-e2e.ps1 @@ -0,0 +1,41 @@ +param( + [switch]$RebuildImage, + [switch]$UseHostOllama +) + +$ErrorActionPreference = 'Stop' + +$toolRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$image = 'openmemory-graphrag-bridge:latest' + +if ($RebuildImage) { + docker build -t $image $toolRoot | Out-Host + if ($LASTEXITCODE -ne 0) { + Write-Error "docker build failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } +} + +$previousMode = $env:OPENMEMORY_GRAPHRAG_USE_HOST_OLLAMA +try { + if ($UseHostOllama) { + $env:OPENMEMORY_GRAPHRAG_USE_HOST_OLLAMA = 'true' + } + else { + Remove-Item Env:OPENMEMORY_GRAPHRAG_USE_HOST_OLLAMA -ErrorAction SilentlyContinue + } + + python "$toolRoot\tests\test_e2e_bridge_stack.py" + if ($LASTEXITCODE -ne 0) { + Write-Error "test_e2e_bridge_stack.py failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } +} +finally { + if ($null -eq $previousMode) { + Remove-Item Env:OPENMEMORY_GRAPHRAG_USE_HOST_OLLAMA -ErrorAction SilentlyContinue + } + else { + $env:OPENMEMORY_GRAPHRAG_USE_HOST_OLLAMA = $previousMode + } +} diff --git a/tools/openmemory-graphrag-bridge/run-tests.ps1 b/tools/openmemory-graphrag-bridge/run-tests.ps1 new file mode 100644 index 00000000..e10cacf9 --- /dev/null +++ b/tools/openmemory-graphrag-bridge/run-tests.ps1 @@ -0,0 +1,27 @@ +param( + [switch]$RebuildImage +) + +$ErrorActionPreference = 'Stop' + +$toolRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$image = 'openmemory-graphrag-bridge:latest' + +if ($RebuildImage) { + docker build -t $image $toolRoot | Out-Host + if ($LASTEXITCODE -ne 0) { + Write-Error "docker build failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } +} + +docker run --rm ` + -v "${toolRoot}:/app" ` + -e "PYTHONWARNINGS=ignore:Support for class-based \`config\` is deprecated" ` + --entrypoint python ` + $image ` + /app/tests/test_server_logic.py +if ($LASTEXITCODE -ne 0) { + Write-Error "test_server_logic.py failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE +} diff --git a/tools/openmemory-graphrag-bridge/server.py b/tools/openmemory-graphrag-bridge/server.py new file mode 100644 index 00000000..e6a8d2a5 --- /dev/null +++ b/tools/openmemory-graphrag-bridge/server.py @@ -0,0 +1,1532 @@ +from __future__ import annotations + +import os +import asyncio +import logging +import re +import io +import time +import warnings +from pathlib import Path +from contextlib import asynccontextmanager, redirect_stderr, redirect_stdout +from typing import Any + +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel, Field + + +logger = logging.getLogger(__name__) +_HF_HTTP_LOGGER_NAME = "huggingface_hub.utils._http" +_HF_UNAUTH_WARNING_SNIPPET = "unauthenticated requests to the HF Hub" + + +class UpsertDocumentRequest(BaseModel): + document_id: str = Field(min_length=1) + content: str = Field(min_length=1) + metadata: dict[str, Any] = Field(default_factory=dict) + user_id: str | None = None + project_id: str | None = None + finalize: bool = False + + +class QueryRequest(BaseModel): + query: str = Field(min_length=1) + k: int = Field(default=8, ge=1, le=32) + user_id: str | None = None + project_id: str | None = None + return_context: bool = True + + +class DeleteDocumentRequest(BaseModel): + document_id: str = Field(min_length=1) + finalize: bool = False + + +class BackfillScopeRequest(BaseModel): + document_ids: list[str] = Field(default_factory=list) + dry_run: bool = False + limit: int = Field(default=1000, ge=1, le=10000) + + +def _env_int(name: str, default: int) -> int: + raw = os.getenv(name) + if not raw: + return default + try: + return int(raw) + except ValueError: + return default + + +async def _tcp_reachable(host: str, port: int, timeout: float = 1.0) -> bool: + try: + reader, writer = await asyncio.wait_for( + asyncio.open_connection(host, port), + timeout=timeout, + ) + writer.close() + await writer.wait_closed() + return True + except Exception: + return False + + +def _missing_model_env() -> list[str]: + required = ["OM_GRAPHRAG_LLM_MODEL", "OM_GRAPHRAG_EMBEDDER_MODEL"] + return [name for name in required if not os.getenv(name)] + + +def _missing_bridge_auth_env() -> list[str]: + return [] if os.getenv("OM_GRAPHRAG_BRIDGE_API_KEY") else ["OM_GRAPHRAG_BRIDGE_API_KEY"] + + +def _require_model_env() -> None: + missing = _missing_model_env() + if missing: + raise RuntimeError( + "Missing GraphRAG model configuration: " + ", ".join(missing) + ) + + +def _require_bridge_auth(request: Request) -> None: + expected = os.getenv("OM_GRAPHRAG_BRIDGE_API_KEY") + if not expected: + raise HTTPException(status_code=503, detail="OM_GRAPHRAG_BRIDGE_API_KEY is required") + provided = request.headers.get("x-graph-api-key") + if provided != expected: + raise HTTPException(status_code=401, detail="invalid GraphRAG bridge API key") + + +def _model_dump(value: Any) -> Any: + if hasattr(value, "model_dump"): + return value.model_dump() + if hasattr(value, "dict"): + return value.dict() + if isinstance(value, list): + return [_model_dump(item) for item in value] + if isinstance(value, dict): + return {key: _model_dump(item) for key, item in value.items()} + return value + + +def _debug_timing_enabled(request: Request) -> bool: + header = request.headers.get("x-graph-debug-timings", "") + if header.lower() in {"1", "true", "yes", "on"}: + return True + return _debug_timing_env_enabled() + + +def _debug_timing_env_enabled() -> bool: + return os.getenv("OM_GRAPHRAG_DEBUG_TIMINGS", "false").lower() == "true" + + +def _emit_debug_timing(event: str, **fields: Any) -> None: + payload = " ".join(f"{key}={value}" for key, value in fields.items()) + logger.warning("[graphrag-debug] %s %s", event, payload) + + +def _with_provenance(req: UpsertDocumentRequest) -> str: + lines = [ + f"OpenMemory document id: {req.document_id}", + f"OpenMemory user id: {req.user_id or 'unknown'}", + f"OpenMemory project id: {req.project_id or 'system_global'}", + ] + source = req.metadata.get("source") + if source: + lines.append(f"OpenMemory source: {source}") + return "\n".join(lines) + "\n\n" + req.content + + +def _load_graphrag(): + try: + from graphrag_sdk import ConnectionConfig, GraphRAG, LiteLLM, LiteLLMEmbedder + from graphrag_sdk.api import main as sdk_main + from graphrag_sdk.core.connection import FalkorDBConnection + from graphrag_sdk.core.models import ChatMessage, RagResult, RetrieverResult, RetrieverResultItem + except Exception as exc: # pragma: no cover - depends on optional install + raise RuntimeError( + "graphrag-sdk is not installed. Install tools/openmemory-graphrag-bridge/requirements.txt." + ) from exc + + return ( + ConnectionConfig, + FalkorDBConnection, + GraphRAG, + LiteLLM, + LiteLLMEmbedder, + sdk_main, + ChatMessage, + RagResult, + RetrieverResult, + RetrieverResultItem, + ) + + +def _retrieval_strategy_for_k(rag: Any, k: int) -> Any: + from graphrag_sdk.retrieval import MultiPathRetrieval + + graph_store = getattr(rag, "_graph_store", None) + vector_store = getattr(rag, "_vector_store", None) + if graph_store is None or vector_store is None: + raise RuntimeError("GraphRAG SDK internals needed for k-limited retrieval are unavailable") + + req_k = k + return MultiPathRetrieval( + graph_store=graph_store, + vector_store=vector_store, + embedder=rag.embedder, + llm=rag.llm, + chunk_top_k=req_k, + rel_top_k=req_k, + max_entities=max(req_k * 2, 8), + max_relationships=max(req_k * 2, 8), + keyword_limit=min(req_k, 10), + ) + + +def _extract_provenance_value(block: str, label: str) -> str | None: + pattern = rf"^{re.escape(label)}\s*(.+)$" + match = re.search(pattern, block, flags=re.MULTILINE) + if not match: + return None + return match.group(1).strip() + + +def _extract_source_doc_id(block: str) -> str | None: + match = re.match(r"^\[Source:\s*([^\]]+)\]", block) + if not match: + return None + return match.group(1).strip() + + +def _scope_matches_block(block: str, user_id: str | None, project_id: str | None) -> bool: + if user_id: + if _extract_provenance_value(block, "OpenMemory user id:") != user_id: + return False + if project_id: + if _extract_provenance_value(block, "OpenMemory project id:") != project_id: + return False + return True + + +def _split_passage_blocks(content: str) -> tuple[str, list[str]]: + text = content.strip() + if not text: + return "", [] + + heading = "" + body = text + if text.startswith("## "): + first_nl = text.find("\n") + if first_nl != -1: + heading = text[:first_nl] + body = text[first_nl + 1 :].lstrip() + + blocks = [ + block.strip() + for block in re.split(r"\n---\n(?=\[Source: )", body) + if block.strip() + ] + return heading, blocks + + +async def _lookup_document_scope_map(rag: Any, doc_ids: list[str]) -> dict[str, dict[str, str | None]]: + graph_store = getattr(rag, "_graph_store", None) + if graph_store is None or not doc_ids: + return {} + + result = await graph_store.query_raw( + "UNWIND $ids AS id " + "MATCH (d:Document {id: id}) " + "RETURN d.id AS id, d.openmemory_user_id AS user_id, d.openmemory_project_id AS project_id", + {"ids": doc_ids}, + ) + scope_map: dict[str, dict[str, str | None]] = {} + for row in result.result_set: + scope_map[row[0]] = { + "user_id": row[1] if len(row) > 1 else None, + "project_id": row[2] if len(row) > 2 else None, + } + return scope_map + + +def _scope_matches_graph_metadata( + doc_scope: dict[str, str | None] | None, + *, + user_id: str | None, + project_id: str | None, +) -> bool: + if not doc_scope: + return False + if user_id and doc_scope.get("user_id") != user_id: + return False + if project_id and doc_scope.get("project_id") != project_id: + return False + return True + + +async def _scoped_chunk_pushdown_retriever( + rag: Any, + req: QueryRequest, + *, + RetrieverResult: Any, + RetrieverResultItem: Any, +) -> Any | None: + from graphrag_sdk.storage.vector_store import _escape_fulltext_query + + graph_store = getattr(rag, "_graph_store", None) + embedder = getattr(rag, "embedder", None) + if graph_store is None or embedder is None: + return None + + query_vector = await embedder.aembed_query(req.query) + top_k = max(req.k * 4, 8) + chunks: dict[str, dict[str, Any]] = {} + + def _add(row: list[Any], source: str) -> None: + if not row: + return + chunk_id = row[0] + text = row[1] if len(row) > 1 else "" + document_id = row[2] if len(row) > 2 else None + score = row[3] if len(row) > 3 else None + if not chunk_id or not text or chunk_id in chunks: + return + chunks[chunk_id] = { + "text": text, + "document_id": document_id, + "source": source, + "score": score, + } + + fulltext_result = await graph_store.query_raw( + "CALL db.idx.fulltext.queryNodes('Chunk', $query_text) " + "YIELD node, score " + "MATCH (d:Document)-[:PART_OF]->(node) " + "WHERE ($user_id IS NULL OR d.openmemory_user_id = $user_id) " + " AND ($project_id IS NULL OR d.openmemory_project_id = $project_id) " + "RETURN node.id AS id, node.text AS text, d.id AS document_id, score " + "ORDER BY score DESC LIMIT $top_k", + { + "query_text": _escape_fulltext_query(req.query), + "top_k": top_k, + "user_id": req.user_id, + "project_id": req.project_id, + }, + ) + for row in fulltext_result.result_set: + _add(row, "fulltext_scoped") + + vector_result = await graph_store.query_raw( + "CALL db.idx.vector.queryNodes('Chunk', 'embedding', $top_k, vecf32($vector)) " + "YIELD node, score " + "MATCH (d:Document)-[:PART_OF]->(node) " + "WHERE ($user_id IS NULL OR d.openmemory_user_id = $user_id) " + " AND ($project_id IS NULL OR d.openmemory_project_id = $project_id) " + "RETURN node.id AS id, node.text AS text, d.id AS document_id, score " + "ORDER BY score DESC LIMIT $top_k", + { + "top_k": top_k, + "vector": query_vector, + "user_id": req.user_id, + "project_id": req.project_id, + }, + ) + for row in vector_result.result_set: + _add(row, "vector_scoped") + + if not chunks: + return None + + passages = [] + for entry in chunks.values(): + source = entry["document_id"] or "unknown" + passages.append(f"[Source: {source}]\n{entry['text']}") + + return RetrieverResult( + items=[ + RetrieverResultItem( + content="## Source Document Passages\n" + "\n---\n".join(passages), + metadata={ + "section": "passages", + "scope_filtered": True, + "scope_filter_mode": "graph_native_chunk_pushdown", + }, + ) + ], + metadata={ + "strategy": "graph_native_chunk_pushdown", + "scope_filtered": True, + "scope_user_id": req.user_id, + "scope_project_id": req.project_id, + "scope_candidate_chunk_count": len(chunks), + "scope_candidate_paths": ["fulltext_scoped", "vector_scoped"], + }, + ) + + +def _merge_scoped_retriever_results( + primary: Any | None, + secondary: Any | None, + *, + RetrieverResult: Any, + RetrieverResultItem: Any, +) -> Any: + if primary is None and secondary is None: + return RetrieverResult(items=[], metadata={}) + + seen_blocks: set[str] = set() + merged_blocks: list[str] = [] + blocks_graph_matched = 0 + blocks_legacy_matched = 0 + candidate_chunk_count = getattr(primary, "metadata", {}).get( + "scope_candidate_chunk_count", 0 + ) + + for item in getattr(primary, "items", []) if primary is not None else []: + if item.metadata.get("section") != "passages": + continue + _heading, blocks = _split_passage_blocks(item.content) + for block in blocks: + if block in seen_blocks: + continue + seen_blocks.add(block) + merged_blocks.append(block) + blocks_graph_matched += 1 + + for item in getattr(secondary, "items", []) if secondary is not None else []: + if item.metadata.get("section") != "passages": + continue + _heading, blocks = _split_passage_blocks(item.content) + for block in blocks: + if block in seen_blocks: + continue + seen_blocks.add(block) + merged_blocks.append(block) + blocks_legacy_matched += 1 + + mode = "none" + if blocks_graph_matched > 0: + mode = "graph_native_chunk_pushdown" + if blocks_legacy_matched > 0: + mode = ( + "graph_native_pushdown_with_legacy_backfill" + if mode == "graph_native_chunk_pushdown" + else "legacy_filtered_retriever" + ) + + if not merged_blocks: + return RetrieverResult( + items=[], + metadata={ + "scope_retrieval_mode": mode, + "scope_blocks_graph_matched": blocks_graph_matched, + "scope_blocks_legacy_matched": blocks_legacy_matched, + "scope_candidate_chunk_count": candidate_chunk_count, + }, + ) + + return RetrieverResult( + items=[ + RetrieverResultItem( + content="## Source Document Passages\n" + "\n---\n".join(merged_blocks), + metadata={ + "section": "passages", + "scope_filtered": True, + "scope_filter_mode": mode, + }, + ) + ], + metadata={ + "scope_retrieval_mode": mode, + "scope_blocks_graph_matched": blocks_graph_matched, + "scope_blocks_legacy_matched": blocks_legacy_matched, + "scope_candidate_chunk_count": candidate_chunk_count, + }, + ) + + +def _finalize_graph_native_retriever_result( + primary: Any | None, + *, + RetrieverResult: Any, + RetrieverResultItem: Any, +) -> Any: + candidate_chunk_count = getattr(primary, "metadata", {}).get( + "scope_candidate_chunk_count", 0 + ) + if primary is None: + return RetrieverResult( + items=[], + metadata={ + "scope_retrieval_mode": "none", + "scope_blocks_graph_matched": 0, + "scope_blocks_legacy_matched": 0, + "scope_candidate_chunk_count": candidate_chunk_count, + }, + ) + + merged_blocks: list[str] = [] + for item in getattr(primary, "items", []): + if item.metadata.get("section") != "passages": + continue + _heading, blocks = _split_passage_blocks(item.content) + merged_blocks.extend(blocks) + + blocks_graph_matched = len(merged_blocks) + if not merged_blocks: + return RetrieverResult( + items=[], + metadata={ + **getattr(primary, "metadata", {}), + "scope_retrieval_mode": "none", + "scope_blocks_graph_matched": 0, + "scope_blocks_legacy_matched": 0, + "scope_candidate_chunk_count": candidate_chunk_count, + }, + ) + + return RetrieverResult( + items=[ + RetrieverResultItem( + content="## Source Document Passages\n" + "\n---\n".join(merged_blocks), + metadata={ + "section": "passages", + "scope_filtered": True, + "scope_filter_mode": "graph_native_chunk_pushdown", + }, + ) + ], + metadata={ + **getattr(primary, "metadata", {}), + "scope_retrieval_mode": "graph_native_chunk_pushdown", + "scope_blocks_graph_matched": blocks_graph_matched, + "scope_blocks_legacy_matched": 0, + "scope_candidate_chunk_count": candidate_chunk_count, + }, + ) + + +async def _filter_retriever_result_by_scope( + rag: Any, + retriever_result: Any, + *, + user_id: str | None, + project_id: str | None, + RetrieverResult: Any, + RetrieverResultItem: Any, +) -> Any: + filtered_items: list[Any] = [] + blocks_seen = 0 + blocks_kept = 0 + blocks_graph_matched = 0 + blocks_legacy_matched = 0 + + doc_ids: list[str] = [] + for item in retriever_result.items: + if item.metadata.get("section") != "passages": + continue + _heading, blocks = _split_passage_blocks(item.content) + for block in blocks: + doc_id = _extract_source_doc_id(block) + if doc_id: + doc_ids.append(doc_id) + doc_scope_map = await _lookup_document_scope_map(rag, sorted(set(doc_ids))) + + for item in retriever_result.items: + section = item.metadata.get("section") + if section != "passages": + continue + + heading, blocks = _split_passage_blocks(item.content) + kept_blocks: list[str] = [] + for block in blocks: + blocks_seen += 1 + doc_id = _extract_source_doc_id(block) + graph_match = _scope_matches_graph_metadata( + doc_scope_map.get(doc_id) if doc_id else None, + user_id=user_id, + project_id=project_id, + ) + legacy_match = False + if not graph_match: + legacy_match = _scope_matches_block(block, user_id, project_id) + + if graph_match or legacy_match: + blocks_kept += 1 + if graph_match: + blocks_graph_matched += 1 + else: + blocks_legacy_matched += 1 + kept_blocks.append(block) + + if not kept_blocks: + continue + + merged = "\n---\n".join(kept_blocks) + if heading: + merged = f"{heading}\n{merged}" + + filtered_items.append( + RetrieverResultItem( + content=merged, + metadata={ + **item.metadata, + "scope_filtered": True, + "scope_filter_mode": "graph_metadata" if blocks_legacy_matched == 0 else "graph_metadata_with_legacy_fallback", + }, + score=item.score, + ) + ) + + return RetrieverResult( + items=filtered_items, + metadata={ + **getattr(retriever_result, "metadata", {}), + "scope_filtered": True, + "scope_user_id": user_id, + "scope_project_id": project_id, + "scope_blocks_seen": blocks_seen, + "scope_blocks_kept": blocks_kept, + "scope_blocks_graph_matched": blocks_graph_matched, + "scope_blocks_legacy_matched": blocks_legacy_matched, + }, + ) + + +async def _apply_scope_metadata( + rag: Any, + *, + document_id: str, + user_id: str | None, + project_id: str | None, + source: str | None, +) -> None: + graph_store = getattr(rag, "_graph_store", None) + if graph_store is None: + return + + await graph_store.query_raw( + "MATCH (d:Document {id: $document_id}) " + "SET d.openmemory_document_id = $document_id, " + " d.openmemory_user_id = $user_id, " + " d.openmemory_project_id = $project_id, " + " d.openmemory_source = $source " + "WITH d " + "OPTIONAL MATCH (d)-[:PART_OF]->(c:Chunk) " + "SET c.openmemory_document_id = $document_id, " + " c.openmemory_user_id = $user_id, " + " c.openmemory_project_id = $project_id, " + " c.openmemory_source = $source", + { + "document_id": document_id, + "user_id": user_id, + "project_id": project_id, + "source": source, + }, + ) + + +def _parse_scope_from_chunk_text(text: str) -> dict[str, str | None]: + return { + "user_id": _extract_provenance_value(text, "OpenMemory user id:"), + "project_id": _extract_provenance_value(text, "OpenMemory project id:"), + "source": _extract_provenance_value(text, "OpenMemory source:"), + } + + +async def _backfill_scope_metadata( + rag: Any, + *, + document_ids: list[str] | None = None, + dry_run: bool = False, + limit: int = 1000, +) -> dict[str, Any]: + graph_store = getattr(rag, "_graph_store", None) + if graph_store is None: + raise RuntimeError("GraphRAG graph_store is unavailable for backfill") + + if document_ids: + result = await graph_store.query_raw( + "UNWIND $ids AS id " + "MATCH (d:Document {id: id})-[:PART_OF]->(c:Chunk) " + "RETURN d.id AS document_id, c.text AS chunk_text " + "LIMIT $limit", + {"ids": document_ids, "limit": limit}, + ) + else: + result = await graph_store.query_raw( + "MATCH (d:Document)-[:PART_OF]->(c:Chunk) " + "WHERE d.openmemory_user_id IS NULL OR d.openmemory_project_id IS NULL " + "RETURN d.id AS document_id, c.text AS chunk_text " + "LIMIT $limit", + {"limit": limit}, + ) + + seen_docs: set[str] = set() + scanned = 0 + backfilled = 0 + skipped = 0 + candidates: list[dict[str, str | None]] = [] + + for row in result.result_set: + document_id = row[0] if row else None + chunk_text = row[1] if len(row) > 1 else None + if not document_id or document_id in seen_docs: + continue + seen_docs.add(document_id) + scanned += 1 + parsed = _parse_scope_from_chunk_text(chunk_text or "") + if not parsed["user_id"] or not parsed["project_id"]: + skipped += 1 + continue + candidate = { + "document_id": document_id, + "user_id": parsed["user_id"], + "project_id": parsed["project_id"], + "source": parsed["source"], + } + candidates.append(candidate) + if not dry_run: + await _apply_scope_metadata( + rag, + document_id=document_id, + user_id=parsed["user_id"], + project_id=parsed["project_id"], + source=parsed["source"], + ) + backfilled += 1 + + return { + "ok": True, + "dry_run": dry_run, + "scanned_documents": scanned, + "backfilled_documents": backfilled, + "skipped_documents": skipped, + "document_ids": [item["document_id"] for item in candidates], + } + + +async def _scoped_completion( + rag: Any, + req: QueryRequest, + *, + sdk_main: Any, + ChatMessage: Any, + RagResult: Any, + RetrieverResult: Any, + RetrieverResultItem: Any, +) -> dict[str, Any]: + strategy = _retrieval_strategy_for_k(rag, req.k) + pushdown = await _scoped_chunk_pushdown_retriever( + rag, + req, + RetrieverResult=RetrieverResult, + RetrieverResultItem=RetrieverResultItem, + ) + if _scope_legacy_backfill_required_state is None: + _update_scope_state(await _scope_gap_counts()) + + legacy_recovery_active = bool( + _scope_legacy_backfill_required_state and _legacy_scope_recovery_enabled() + ) + + if not legacy_recovery_active: + filtered = _finalize_graph_native_retriever_result( + pushdown, + RetrieverResult=RetrieverResult, + RetrieverResultItem=RetrieverResultItem, + ) + else: + legacy = None + retriever_result = await rag.retrieve(req.query, strategy=strategy) + legacy = await _filter_retriever_result_by_scope( + rag, + retriever_result, + user_id=req.user_id, + project_id=req.project_id, + RetrieverResult=RetrieverResult, + RetrieverResultItem=RetrieverResultItem, + ) + filtered = _merge_scoped_retriever_results( + pushdown, + legacy, + RetrieverResult=RetrieverResult, + RetrieverResultItem=RetrieverResultItem, + ) + retrieval_mode = filtered.metadata.get("scope_retrieval_mode", "none") + + if not filtered.items: + meta = getattr(filtered, "metadata", {}) + return { + "ok": False, + "error": "scoped query returned no scope-matching context", + "scope_enforced": True, + "scope_retrieval_mode": retrieval_mode, + "scope_legacy_recovery_enabled": _legacy_scope_recovery_enabled(), + "user_id": req.user_id, + "project_id": req.project_id, + "scope_blocks_seen": meta.get("scope_blocks_seen", 0), + "scope_blocks_kept": meta.get("scope_blocks_kept", 0), + "scope_blocks_graph_matched": meta.get("scope_blocks_graph_matched", 0), + "scope_blocks_legacy_matched": meta.get("scope_blocks_legacy_matched", 0), + "scope_candidate_chunk_count": meta.get("scope_candidate_chunk_count", 0), + } + + context_str = "\n---\n".join( + sdk_main._neutralize_context_close_tag(item.content) + for item in filtered.items + ) + messages = [ + ChatMessage(role="system", content=sdk_main._RAG_SYSTEM_PROMPT_DELIMITED), + ChatMessage( + role="user", + content=sdk_main._RAG_PROMPT.format( + context=context_str, + question=req.query, + ), + ), + ] + llm_response = await rag.llm.ainvoke_messages(messages) + result = RagResult( + answer=rag._clean_answer(llm_response.content), + retriever_result=filtered if req.return_context else None, + metadata={ + "model": rag.llm.model_name, + "num_context_items": len(filtered.items), + "strategy": strategy.__class__.__name__, + "has_history": False, + "retrieval_query": req.query, + "scope_enforced": True, + "scope_retrieval_mode": retrieval_mode, + "scope_legacy_recovery_enabled": _legacy_scope_recovery_enabled(), + "scope_user_id": req.user_id, + "scope_project_id": req.project_id, + "scope_blocks_seen": filtered.metadata.get("scope_blocks_seen", 0), + "scope_blocks_kept": filtered.metadata.get("scope_blocks_kept", 0), + "scope_blocks_graph_matched": filtered.metadata.get("scope_blocks_graph_matched", 0), + "scope_blocks_legacy_matched": filtered.metadata.get("scope_blocks_legacy_matched", 0), + "scope_candidate_chunk_count": filtered.metadata.get("scope_candidate_chunk_count", 0), + }, + ) + return { + "ok": True, + "query": req.query, + "k": req.k, + "k_applied": True, + "user_id": req.user_id, + "project_id": req.project_id, + "scope_enforced": True, + "scope_retrieval_mode": retrieval_mode, + "scope_legacy_recovery_enabled": _legacy_scope_recovery_enabled(), + "result": _model_dump(result), + } + + +@asynccontextmanager +async def _rag_context(): + _require_model_env() + ( + ConnectionConfig, + _FalkorDBConnection, + GraphRAG, + LiteLLM, + LiteLLMEmbedder, + _sdk_main, + _ChatMessage, + _RagResult, + _RetrieverResult, + _RetrieverResultItem, + ) = _load_graphrag() + + api_base = os.getenv("OM_GRAPHRAG_API_BASE") or os.getenv("OLLAMA_API_BASE") + llm_kwargs: dict[str, Any] = {} + embedder_kwargs: dict[str, Any] = {} + if api_base: + llm_kwargs["api_base"] = api_base + embedder_kwargs["api_base"] = api_base + + llm_max_tokens = os.getenv("OM_GRAPHRAG_LLM_MAX_TOKENS") + if llm_max_tokens: + llm_kwargs["max_tokens"] = int(llm_max_tokens) + + embedding_dimension = 256 + dimensions = os.getenv("OM_GRAPHRAG_EMBEDDER_DIMENSIONS") + if dimensions: + embedding_dimension = int(dimensions) + + async with GraphRAG( + connection=ConnectionConfig( + host=os.getenv("FALKORDB_HOST", "localhost"), + port=_env_int("FALKORDB_PORT", 6379), + username=os.getenv("FALKORDB_USERNAME") or None, + password=os.getenv("FALKORDB_PASSWORD") or None, + graph_name=os.getenv("OM_GRAPHRAG_GRAPH_NAME", "openmemory"), + ), + llm=LiteLLM(model=os.getenv("OM_GRAPHRAG_LLM_MODEL"), **llm_kwargs), + embedder=LiteLLMEmbedder( + model=os.getenv("OM_GRAPHRAG_EMBEDDER_MODEL"), + **embedder_kwargs, + ), + embedding_dimension=embedding_dimension, + ) as rag: + yield rag + + +async def _scope_gap_counts() -> dict[str, int] | None: + try: + ( + ConnectionConfig, + FalkorDBConnection, + _GraphRAG, + _LiteLLM, + _LiteLLMEmbedder, + _sdk_main, + _ChatMessage, + _RagResult, + _RetrieverResult, + _RetrieverResultItem, + ) = _load_graphrag() + conn = FalkorDBConnection( + ConnectionConfig( + host=os.getenv("FALKORDB_HOST", "localhost"), + port=_env_int("FALKORDB_PORT", 6379), + username=os.getenv("FALKORDB_USERNAME") or None, + password=os.getenv("FALKORDB_PASSWORD") or None, + graph_name=os.getenv("OM_GRAPHRAG_GRAPH_NAME", "openmemory"), + ) + ) + docs_total = await conn.query("MATCH (d:Document) RETURN count(d)") + docs_missing = await conn.query( + "MATCH (d:Document) WHERE d.openmemory_project_id IS NULL OR d.openmemory_user_id IS NULL RETURN count(d)" + ) + chunks_total = await conn.query("MATCH (c:Chunk) RETURN count(c)") + chunks_missing = await conn.query( + "MATCH (c:Chunk) WHERE c.openmemory_project_id IS NULL OR c.openmemory_user_id IS NULL RETURN count(c)" + ) + return { + "documents_total": docs_total.result_set[0][0] if docs_total.result_set else 0, + "documents_missing_scope": docs_missing.result_set[0][0] if docs_missing.result_set else 0, + "chunks_total": chunks_total.result_set[0][0] if chunks_total.result_set else 0, + "chunks_missing_scope": chunks_missing.result_set[0][0] if chunks_missing.result_set else 0, + } + except Exception: + return None + + +app = FastAPI(title="OpenMemory GraphRAG Bridge", version="0.1.0") +_gliner_prewarm_status = "not_started" +_gliner_prewarm_error: str | None = None +_gliner_offline_after_prewarm = False +_gliner_local_model_path: str | None = None +_gliner_patch_installed = False +_gliner_debug_patch_installed = False +_litellm_debug_patch_installed = False +_hf_warning_filter_patch_installed = False +_hf_warning_logger_filter: logging.Filter | None = None +_scope_gap_counts_state: dict[str, int] | None = None +_scope_legacy_backfill_required_state: bool | None = None + + +def _update_scope_state(scope_counts: dict[str, int] | None) -> None: + global _scope_gap_counts_state, _scope_legacy_backfill_required_state + _scope_gap_counts_state = scope_counts + _scope_legacy_backfill_required_state = ( + None + if scope_counts is None + else bool( + scope_counts["documents_missing_scope"] > 0 + or scope_counts["chunks_missing_scope"] > 0 + ) + ) + + +def _scope_health_contract_fields(scope_legacy_required: bool | None) -> dict[str, Any]: + scope_storage_contract = ( + "document_chunk_properties_with_legacy_fallback" + if scope_legacy_required in (None, True) + else "document_chunk_properties" + ) + return { + "scope_storage_contract": scope_storage_contract, + "scope_operator_recovery_path_present": True, + # Deprecated compatibility alias. Kept to avoid breaking existing consumers. + "scope_storage_compatibility_path_present": True, + } + + +def _gliner_model_name() -> str: + return os.getenv("OM_GRAPHRAG_GLINER_MODEL", "urchade/gliner_medium-v2.1") + + +def _gliner_cache_dir() -> str | None: + return os.getenv("OM_GRAPHRAG_GLINER_CACHE_DIR") or os.getenv("HF_HOME") + + +def _gliner_prewarm_enabled() -> bool: + return os.getenv("OM_GRAPHRAG_GLINER_PREWARM", "true").lower() == "true" + + +def _gliner_online_fallback_enabled() -> bool: + return os.getenv("OM_GRAPHRAG_GLINER_ALLOW_ONLINE_FALLBACK", "false").lower() == "true" + + +def _legacy_scope_recovery_enabled() -> bool: + return os.getenv("OM_GRAPHRAG_ENABLE_LEGACY_SCOPE_RECOVERY", "false").lower() == "true" + + +def _test_disable_extraction() -> bool: + return os.getenv("OM_GRAPHRAG_TEST_DISABLE_EXTRACTION", "false").lower() == "true" + + +def _test_extractor_override() -> Any | None: + if not _test_disable_extraction(): + return None + + class _NoOpExtractionStrategy: + async def extract(self, chunks: Any, schema: Any, ctx: Any) -> Any: + from graphrag_sdk.core.models import GraphData + + if ctx is not None: + try: + ctx.log("Using no-op GraphRAG extraction override") + except Exception: + pass + + return GraphData( + nodes=[], + relationships=[], + mentions=[], + extracted_entities=[], + extracted_relations=[], + ) + + return _NoOpExtractionStrategy() + + +class _HFWarningMessageFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + return _HF_UNAUTH_WARNING_SNIPPET not in record.getMessage() + + +def _install_hf_warning_filter_patch() -> None: + global _hf_warning_filter_patch_installed, _hf_warning_logger_filter + if _hf_warning_filter_patch_installed: + return + + target_logger = logging.getLogger(_HF_HTTP_LOGGER_NAME) + if _hf_warning_logger_filter is None: + _hf_warning_logger_filter = _HFWarningMessageFilter() + if _hf_warning_logger_filter not in target_logger.filters: + target_logger.addFilter(_hf_warning_logger_filter) + _hf_warning_filter_patch_installed = True + + +def _gliner_cache_snapshot_path(model_name: str, cache_dir: str | None) -> str | None: + if not cache_dir or "/" not in model_name: + return None + + namespace, repo = model_name.split("/", 1) + repo_dir = Path(cache_dir) / f"models--{namespace}--{repo}" + snapshots_dir = repo_dir / "snapshots" + refs_dir = repo_dir / "refs" + + if refs_dir.exists(): + for ref_name in ("main", "master"): + ref_file = refs_dir / ref_name + if ref_file.exists(): + commit = ref_file.read_text(encoding="utf-8").strip() + candidate = snapshots_dir / commit + if candidate.exists(): + return str(candidate) + + if snapshots_dir.exists(): + candidates = [path for path in snapshots_dir.iterdir() if path.is_dir()] + if candidates: + candidates.sort(key=lambda path: path.stat().st_mtime, reverse=True) + return str(candidates[0]) + + return None + + +def _resolve_gliner_local_path(*, allow_download: bool) -> str | None: + configured = os.getenv("OM_GRAPHRAG_GLINER_LOCAL_PATH") + if configured and os.path.exists(configured): + return configured + + model_name = _gliner_model_name() + if os.path.exists(model_name): + return model_name + + cache_hit = _gliner_cache_snapshot_path(model_name, _gliner_cache_dir()) + if cache_hit: + return cache_hit + + if not allow_download: + return None + + try: + from huggingface_hub import snapshot_download + + return snapshot_download( + model_name, + cache_dir=_gliner_cache_dir(), + local_files_only=not allow_download, + ) + except Exception: + return None + + +def _install_gliner_local_path_patch() -> None: + global _gliner_patch_installed + if _gliner_patch_installed or not _gliner_local_model_path: + return + + from graphrag_sdk.ingestion.extraction_strategies.entity_extractors import GLiNERExtractor + + original_init = GLiNERExtractor.__init__ + + def patched_init(self, threshold: float = 0.75, model_name: str = "urchade/gliner_medium-v2.1") -> None: + effective = model_name + if model_name == "urchade/gliner_medium-v2.1" and _gliner_local_model_path: + effective = _gliner_local_model_path + original_init(self, threshold=threshold, model_name=effective) + + GLiNERExtractor.__init__ = patched_init # type: ignore[assignment] + _gliner_patch_installed = True + + +def _install_gliner_debug_patch() -> None: + global _gliner_debug_patch_installed + if _gliner_debug_patch_installed or not _debug_timing_env_enabled(): + return + + from graphrag_sdk.ingestion.extraction_strategies.entity_extractors import GLiNERExtractor + + original_load_model = GLiNERExtractor._load_model + original_predict_sync = GLiNERExtractor._predict_sync + + def patched_load_model(self: Any) -> Any: + started = time.perf_counter() + _emit_debug_timing( + "gliner_load_model_start", + model_name=getattr(self, "_model_name", "unknown"), + ) + try: + model = original_load_model(self) + _emit_debug_timing( + "gliner_load_model_done", + model_name=getattr(self, "_model_name", "unknown"), + elapsed_ms=round((time.perf_counter() - started) * 1000, 2), + ) + return model + except Exception as exc: + _emit_debug_timing( + "gliner_load_model_error", + model_name=getattr(self, "_model_name", "unknown"), + elapsed_ms=round((time.perf_counter() - started) * 1000, 2), + error=repr(exc), + ) + raise + + def patched_predict_sync(self: Any, text: str, entity_types: list[str]) -> list[dict[str, Any]]: + started = time.perf_counter() + _emit_debug_timing( + "gliner_predict_start", + model_name=getattr(self, "_model_name", "unknown"), + text_len=len(text), + entity_types=len(entity_types), + ) + try: + result = original_predict_sync(self, text, entity_types) + _emit_debug_timing( + "gliner_predict_done", + model_name=getattr(self, "_model_name", "unknown"), + elapsed_ms=round((time.perf_counter() - started) * 1000, 2), + entities=len(result), + ) + return result + except Exception as exc: + _emit_debug_timing( + "gliner_predict_error", + model_name=getattr(self, "_model_name", "unknown"), + elapsed_ms=round((time.perf_counter() - started) * 1000, 2), + error=repr(exc), + ) + raise + + GLiNERExtractor._load_model = patched_load_model # type: ignore[assignment] + GLiNERExtractor._predict_sync = patched_predict_sync # type: ignore[assignment] + _gliner_debug_patch_installed = True + + +def _install_litellm_debug_patch() -> None: + global _litellm_debug_patch_installed + if _litellm_debug_patch_installed or not _debug_timing_env_enabled(): + return + + from graphrag_sdk import LiteLLM, LiteLLMEmbedder + + original_ainvoke = LiteLLM.ainvoke + original_embed_async = LiteLLMEmbedder._raw_embed_async + + async def patched_ainvoke(self: Any, prompt: str, *args: Any, **kwargs: Any) -> Any: + started = time.perf_counter() + _emit_debug_timing( + "litellm_ainvoke_start", + model_name=getattr(self, "model_name", "unknown"), + prompt_len=len(prompt), + ) + try: + result = await original_ainvoke(self, prompt, *args, **kwargs) + _emit_debug_timing( + "litellm_ainvoke_done", + model_name=getattr(self, "model_name", "unknown"), + elapsed_ms=round((time.perf_counter() - started) * 1000, 2), + ) + return result + except Exception as exc: + _emit_debug_timing( + "litellm_ainvoke_error", + model_name=getattr(self, "model_name", "unknown"), + elapsed_ms=round((time.perf_counter() - started) * 1000, 2), + error=repr(exc), + ) + raise + + async def patched_embed_async(self: Any, texts: list[str], *args: Any, **kwargs: Any) -> list[list[float]]: + started = time.perf_counter() + _emit_debug_timing( + "litellm_embed_start", + model_name=getattr(self, "model", "unknown"), + batch_size=len(texts), + total_chars=sum(len(text) for text in texts), + ) + try: + result = await original_embed_async(self, texts, *args, **kwargs) + _emit_debug_timing( + "litellm_embed_done", + model_name=getattr(self, "model", "unknown"), + elapsed_ms=round((time.perf_counter() - started) * 1000, 2), + vectors=len(result), + ) + return result + except Exception as exc: + _emit_debug_timing( + "litellm_embed_error", + model_name=getattr(self, "model", "unknown"), + elapsed_ms=round((time.perf_counter() - started) * 1000, 2), + error=repr(exc), + ) + raise + + LiteLLM.ainvoke = patched_ainvoke # type: ignore[assignment] + LiteLLMEmbedder._raw_embed_async = patched_embed_async # type: ignore[assignment] + _litellm_debug_patch_installed = True + + +def _prewarm_gliner_sync() -> None: + from gliner import GLiNER + + global _gliner_local_model_path + cache_dir = _gliner_cache_dir() + old_hf_offline = os.environ.get("HF_HUB_OFFLINE") + old_tf_offline = os.environ.get("TRANSFORMERS_OFFLINE") + local_path = _resolve_gliner_local_path(allow_download=False) + if local_path: + _gliner_local_model_path = local_path + _install_gliner_local_path_patch() + sink = io.StringIO() + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message=r".*resume_download.*deprecated.*", + ) + with redirect_stderr(sink), redirect_stdout(sink): + try: + os.environ["HF_HUB_OFFLINE"] = "1" + os.environ["TRANSFORMERS_OFFLINE"] = "1" + GLiNER.from_pretrained( + _gliner_local_model_path or _gliner_model_name(), + cache_dir=cache_dir, + local_files_only=True, + ) + return + except Exception: + if old_hf_offline is None: + os.environ.pop("HF_HUB_OFFLINE", None) + else: + os.environ["HF_HUB_OFFLINE"] = old_hf_offline + if old_tf_offline is None: + os.environ.pop("TRANSFORMERS_OFFLINE", None) + else: + os.environ["TRANSFORMERS_OFFLINE"] = old_tf_offline + if local_path and not _gliner_online_fallback_enabled(): + raise RuntimeError( + "cached local GLiNER prewarm failed and online fallback is disabled" + ) + if not local_path and not _gliner_online_fallback_enabled(): + raise RuntimeError( + "GLiNER cache miss and online fallback is disabled" + ) + downloaded = GLiNER.from_pretrained(_gliner_model_name(), cache_dir=cache_dir) + if _gliner_local_model_path is None: + resolved = _resolve_gliner_local_path(allow_download=False) + if resolved: + _gliner_local_model_path = resolved + _install_gliner_local_path_patch() + return downloaded + + +async def _prewarm_gliner() -> None: + global _gliner_prewarm_status, _gliner_prewarm_error, _gliner_offline_after_prewarm + _gliner_prewarm_status = "running" + _gliner_prewarm_error = None + try: + await asyncio.to_thread(_prewarm_gliner_sync) + os.environ["HF_HUB_OFFLINE"] = "1" + os.environ["TRANSFORMERS_OFFLINE"] = "1" + _gliner_offline_after_prewarm = True + _gliner_prewarm_status = "ready" + except Exception as exc: # pragma: no cover - best-effort warm path + _gliner_prewarm_status = "failed" + _gliner_prewarm_error = str(exc) + + +@app.on_event("startup") +async def startup_event() -> None: + _install_hf_warning_filter_patch() + _install_gliner_debug_patch() + _install_litellm_debug_patch() + if _gliner_prewarm_enabled(): + asyncio.create_task(_prewarm_gliner()) + + +@app.get("/health") +async def health() -> dict[str, Any]: + missing_model_env = _missing_model_env() + missing_bridge_auth_env = _missing_bridge_auth_env() + falkordb_host = os.getenv("FALKORDB_HOST", "localhost") + falkordb_port = _env_int("FALKORDB_PORT", 6379) + falkordb_reachable = await _tcp_reachable(falkordb_host, falkordb_port) + try: + _load_graphrag() + import_available = True + error = None + except RuntimeError as exc: + import_available = False + error = str(exc) + scope_counts = await _scope_gap_counts() if import_available and falkordb_reachable else None + _update_scope_state(scope_counts) + scope_legacy_required = _scope_legacy_backfill_required_state + scope_contract_fields = _scope_health_contract_fields(scope_legacy_required) + + return { + "ok": import_available and not missing_model_env and not missing_bridge_auth_env and falkordb_reachable, + "graphrag_available": import_available, + "configured": not missing_model_env and not missing_bridge_auth_env, + "missing_model_env": missing_model_env, + "bridge_auth_configured": not missing_bridge_auth_env, + "missing_bridge_auth_env": missing_bridge_auth_env, + "falkordb_reachable": falkordb_reachable, + "scope_query_filtering": True, + **scope_contract_fields, + "scope_legacy_recovery_enabled": _legacy_scope_recovery_enabled(), + "test_disable_extraction": _test_disable_extraction(), + "gliner_model": _gliner_model_name(), + "gliner_local_model_path": _gliner_local_model_path, + "gliner_cache_dir": _gliner_cache_dir(), + "gliner_prewarm_enabled": _gliner_prewarm_enabled(), + "gliner_online_fallback_enabled": _gliner_online_fallback_enabled(), + "gliner_prewarm_status": _gliner_prewarm_status, + "gliner_prewarm_error": _gliner_prewarm_error, + "gliner_offline_after_prewarm": _gliner_offline_after_prewarm, + "hf_warning_filter_patch_installed": _hf_warning_filter_patch_installed, + "scope_gap_counts": scope_counts, + "scope_legacy_backfill_required": scope_legacy_required, + "error": error, + "graph_name": os.getenv("OM_GRAPHRAG_GRAPH_NAME", "openmemory"), + "falkordb_host": falkordb_host, + "falkordb_port": falkordb_port, + } + + +@app.post("/documents/upsert") +async def upsert_document(request: Request, req: UpsertDocumentRequest) -> dict[str, Any]: + _require_bridge_auth(request) + debug_timing = _debug_timing_enabled(request) + started = time.perf_counter() + phase_started = started + timings_ms: dict[str, float] = {} + + def record_phase(name: str) -> None: + nonlocal phase_started + now = time.perf_counter() + elapsed_ms = round((now - phase_started) * 1000, 2) + timings_ms[name] = elapsed_ms + phase_started = now + if debug_timing: + _emit_debug_timing( + "phase", + document_id=req.document_id, + phase=name, + elapsed_ms=elapsed_ms, + total_ms=round((now - started) * 1000, 2), + ) + + try: + if debug_timing: + _emit_debug_timing( + "start", + document_id=req.document_id, + finalize=req.finalize, + user_id=req.user_id or "unknown", + project_id=req.project_id or "system_global", + ) + async with _rag_context() as rag: + record_phase("rag_context_entered") + extractor = _test_extractor_override() + metadata = { + **req.metadata, + "openmemory_user_id": req.user_id, + "openmemory_project_id": req.project_id, + "openmemory_document_id": req.document_id, + } + + result = await rag.update( + text=_with_provenance(req), + document_id=req.document_id, + extractor=extractor, + if_missing="ingest", + ctx=None, + ) + record_phase("rag_update") + await _apply_scope_metadata( + rag, + document_id=req.document_id, + user_id=req.user_id, + project_id=req.project_id, + source=req.metadata.get("source"), + ) + record_phase("scope_metadata") + if hasattr(result, "metadata") and isinstance(result.metadata, dict): + result.metadata.update(metadata) + + finalize_result = None + if req.finalize: + finalize_result = await rag.finalize() + record_phase("finalize") + + total_ms = round((time.perf_counter() - started) * 1000, 2) + if debug_timing: + _emit_debug_timing( + "done", + document_id=req.document_id, + total_ms=total_ms, + ) + return { + "ok": True, + "document_id": req.document_id, + "ingestion": _model_dump(result), + "finalize": _model_dump(finalize_result), + "timings_ms": timings_ms if debug_timing else None, + "total_ms": total_ms if debug_timing else None, + } + except Exception as exc: + if debug_timing: + _emit_debug_timing( + "error", + document_id=req.document_id, + phase=list(timings_ms.keys())[-1] if timings_ms else "start", + error=repr(exc), + total_ms=round((time.perf_counter() - started) * 1000, 2), + ) + raise HTTPException(status_code=502, detail=str(exc)) from exc + + +@app.post("/documents/delete") +async def delete_document(request: Request, req: DeleteDocumentRequest) -> dict[str, Any]: + _require_bridge_auth(request) + try: + async with _rag_context() as rag: + result = await rag.delete_document(req.document_id, if_missing="ignore") + + finalize_result = None + if req.finalize: + finalize_result = await rag.finalize() + + return { + "ok": True, + "document_id": req.document_id, + "deletion": _model_dump(result), + "finalize": _model_dump(finalize_result), + } + except Exception as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + +@app.post("/scope/backfill") +async def backfill_scope(request: Request, req: BackfillScopeRequest) -> dict[str, Any]: + _require_bridge_auth(request) + try: + async with _rag_context() as rag: + result = await _backfill_scope_metadata( + rag, + document_ids=req.document_ids or None, + dry_run=req.dry_run, + limit=req.limit, + ) + if not req.dry_run: + _update_scope_state(await _scope_gap_counts()) + return result + except Exception as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + +@app.post("/query") +async def query(request: Request, req: QueryRequest) -> dict[str, Any]: + _require_bridge_auth(request) + try: + ( + _ConnectionConfig, + _FalkorDBConnection, + _GraphRAG, + _LiteLLM, + _LiteLLMEmbedder, + sdk_main, + ChatMessage, + RagResult, + RetrieverResult, + RetrieverResultItem, + ) = _load_graphrag() + async with _rag_context() as rag: + if req.user_id or req.project_id: + return await _scoped_completion( + rag, + req, + sdk_main=sdk_main, + ChatMessage=ChatMessage, + RagResult=RagResult, + RetrieverResult=RetrieverResult, + RetrieverResultItem=RetrieverResultItem, + ) + + strategy = _retrieval_strategy_for_k(rag, req.k) + result = await rag.completion( + req.query, + strategy=strategy, + return_context=req.return_context, + ) + payload = _model_dump(result) + return { + "ok": True, + "query": req.query, + "k": req.k, + "k_applied": True, + "user_id": req.user_id, + "project_id": req.project_id, + "scope_enforced": False, + "result": payload, + } + except Exception as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc diff --git a/tools/openmemory-graphrag-bridge/tests/test_e2e_bridge_stack.py b/tools/openmemory-graphrag-bridge/tests/test_e2e_bridge_stack.py new file mode 100644 index 00000000..d07da456 --- /dev/null +++ b/tools/openmemory-graphrag-bridge/tests/test_e2e_bridge_stack.py @@ -0,0 +1,332 @@ +from __future__ import annotations + +import contextlib +import json +import os +import re +import subprocess +import sys +import threading +import time +import urllib.request +import uuid +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + + +NETWORK = f"openmemory-e2e-{uuid.uuid4().hex[:8]}" +FALKOR = f"openmemory-e2e-falkor-{uuid.uuid4().hex[:8]}" +BRIDGE = f"openmemory-e2e-bridge-{uuid.uuid4().hex[:8]}" +BRIDGE_KEY = f"e2e-{uuid.uuid4().hex}" +BRIDGE_PORT = 8773 +IMAGE = os.environ.get("OPENMEMORY_GRAPHRAG_BRIDGE_IMAGE", "openmemory-graphrag-bridge:latest") +GRAPH_NAME = f"project_astra_e2e_{uuid.uuid4().hex[:8]}" +USE_HOST_OLLAMA = os.environ.get("OPENMEMORY_GRAPHRAG_USE_HOST_OLLAMA", "").lower() in { + "1", + "true", + "yes", + "on", +} +REQUEST_TIMEOUT = int( + os.environ.get( + "OPENMEMORY_GRAPHRAG_E2E_TIMEOUT", + "300" if USE_HOST_OLLAMA else "120", + ) +) +LLM_MODEL = os.environ.get("OM_GRAPHRAG_LLM_MODEL", "ollama/qwen2.5:1.5b") +EMBED_MODEL = os.environ.get("OM_GRAPHRAG_EMBEDDER_MODEL", "ollama/bge-m3:latest") +EMBED_DIMS = int(os.environ.get("OM_GRAPHRAG_EMBEDDER_DIMENSIONS", "1024")) +HF_CACHE_VOLUME = os.environ.get("OPENMEMORY_GRAPHRAG_HF_CACHE_VOLUME", "openmemory_graphrag_bridge_hf_cache") +HOST_MODE_LLM_MAX_TOKENS = os.environ.get("OPENMEMORY_GRAPHRAG_HOST_LLM_MAX_TOKENS", "256") +_MARKER_PATTERN = re.compile(r"E2E-MARKER-[A-Za-z0-9]+") + + +def run(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]: + return subprocess.run(cmd, check=check, capture_output=True, text=True) + + +def cleanup() -> None: + for name in (BRIDGE, FALKOR): + run(["docker", "rm", "-f", name], check=False) + run(["docker", "network", "rm", NETWORK], check=False) + + +def http_json(method: str, url: str, payload: dict | None = None, headers: dict | None = None, timeout: int = 120) -> dict: + body = None if payload is None else json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + url, + data=body, + method=method, + headers={ + "Content-Type": "application/json", + **(headers or {}), + }, + ) + with urllib.request.urlopen(req, timeout=timeout) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def wait_health(url: str, timeout_s: int = 180) -> dict: + deadline = time.time() + timeout_s + last_error = None + while time.time() < deadline: + try: + payload = http_json("GET", url, timeout=20) + if payload.get("ok"): + return payload + last_error = payload + except Exception as exc: # pragma: no cover - runtime I/O + last_error = str(exc) + time.sleep(2) + raise RuntimeError(f"health did not become ready: {last_error}") + + +def wait_gliner_ready(url: str, timeout_s: int = 300) -> dict: + deadline = time.time() + timeout_s + last_payload = None + while time.time() < deadline: + payload = http_json("GET", url, timeout=20) + last_payload = payload + if payload.get("gliner_prewarm_status") in {"ready", "failed"}: + return payload + time.sleep(2) + raise RuntimeError(f"gliner prewarm did not settle: {last_payload}") + + +class _FakeOllamaHandler(BaseHTTPRequestHandler): + server_version = "FakeOllama/1.0" + sys_version = "" + + def log_message(self, format: str, *args: object) -> None: # pragma: no cover - noisy runtime path + return + + def _read_json(self) -> dict: + size = int(self.headers.get("Content-Length", "0")) + raw = self.rfile.read(size) if size else b"{}" + return json.loads(raw.decode("utf-8") or "{}") + + def _write_json(self, payload: dict) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_POST(self) -> None: # noqa: N802 + payload = self._read_json() + if self.path == "/api/embed": + prompts = payload.get("input", []) + if isinstance(prompts, str): + prompts = [prompts] + dims = int(payload.get("dimensions") or EMBED_DIMS) + embeddings = [] + for index, prompt in enumerate(prompts): + vector = [0.0] * dims + vector[index % dims] = 1.0 + vector[len(str(prompt)) % dims] = 0.5 + embeddings.append(vector) + self._write_json( + { + "model": payload.get("model", EMBED_MODEL), + "embeddings": embeddings, + "prompt_eval_count": max(1, sum(len(str(prompt)) for prompt in prompts) // 8), + } + ) + return + + if self.path == "/api/chat": + messages = payload.get("messages", []) + merged = "\n".join(str(message.get("content", "")) for message in messages if isinstance(message, dict)) + marker_match = _MARKER_PATTERN.search(merged) + content = f"Marker: {marker_match.group(0)}" if marker_match else merged + self._write_json( + { + "model": payload.get("model", LLM_MODEL), + "message": { + "role": "assistant", + "content": content, + }, + "done": True, + "done_reason": "stop", + "prompt_eval_count": max(1, len(merged) // 8), + "eval_count": max(1, len(content) // 8), + } + ) + return + + if self.path == "/api/generate": + prompt = str(payload.get("prompt", "")) + marker_match = _MARKER_PATTERN.search(prompt) + response = f"Marker: {marker_match.group(0)}" if marker_match else prompt + self._write_json( + { + "model": payload.get("model", LLM_MODEL), + "response": response, + "done": True, + "done_reason": "stop", + "prompt_eval_count": max(1, len(prompt) // 8), + "eval_count": max(1, len(response) // 8), + } + ) + return + + self.send_error(404, f"Unsupported path: {self.path}") + + +@contextlib.contextmanager +def fake_ollama_server() -> str: + server = ThreadingHTTPServer(("0.0.0.0", 0), _FakeOllamaHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://host.docker.internal:{server.server_address[1]}" + finally: + server.shutdown() + thread.join(timeout=10) + server.server_close() + + +def main() -> int: + cleanup() + server_ctx = contextlib.nullcontext(os.environ.get("OLLAMA_API_BASE", "http://host.docker.internal:11434")) + if not USE_HOST_OLLAMA: + server_ctx = fake_ollama_server() + + with server_ctx as ollama_api_base: + try: + run(["docker", "network", "create", NETWORK]) + run(["docker", "run", "-d", "--rm", "--name", FALKOR, "--network", NETWORK, "falkordb/falkordb:latest"]) + bridge_cmd = [ + "docker", + "run", + "-d", + "--rm", + "--name", + BRIDGE, + "--network", + NETWORK, + "--add-host", + "host.docker.internal:host-gateway", + "-p", + f"127.0.0.1:{BRIDGE_PORT}:8765", + "-v", + f"{HF_CACHE_VOLUME}:/root/.cache/huggingface", + "-e", + f"FALKORDB_HOST={FALKOR}", + "-e", + "FALKORDB_PORT=6379", + "-e", + f"OM_GRAPHRAG_BRIDGE_API_KEY={BRIDGE_KEY}", + "-e", + f"OM_GRAPHRAG_GRAPH_NAME={GRAPH_NAME}", + "-e", + f"OM_GRAPHRAG_LLM_MODEL={LLM_MODEL}", + "-e", + f"OM_GRAPHRAG_EMBEDDER_MODEL={EMBED_MODEL}", + "-e", + f"OM_GRAPHRAG_EMBEDDER_DIMENSIONS={EMBED_DIMS}", + "-e", + f"OLLAMA_API_BASE={ollama_api_base}", + ] + if USE_HOST_OLLAMA: + bridge_cmd.extend( + [ + "-e", + "OM_GRAPHRAG_GLINER_PREWARM=true", + "-e", + "OM_GRAPHRAG_DEBUG_TIMINGS=true", + "-e", + f"OM_GRAPHRAG_LLM_MAX_TOKENS={HOST_MODE_LLM_MAX_TOKENS}", + ] + ) + else: + bridge_cmd.extend( + [ + "-e", + "OM_GRAPHRAG_GLINER_PREWARM=false", + "-e", + "OM_GRAPHRAG_TEST_DISABLE_EXTRACTION=true", + "-e", + "HF_HUB_OFFLINE=1", + "-e", + "TRANSFORMERS_OFFLINE=1", + ] + ) + bridge_cmd.append(IMAGE) + run(bridge_cmd) + + health = wait_health(f"http://127.0.0.1:{BRIDGE_PORT}/health") + assert health["graphrag_available"] is True + assert health["configured"] is True + assert health["falkordb_reachable"] is True + if USE_HOST_OLLAMA: + health = wait_gliner_ready(f"http://127.0.0.1:{BRIDGE_PORT}/health") + else: + assert health["gliner_prewarm_enabled"] is False + assert health["test_disable_extraction"] is True + + auth_headers = {"x-graph-api-key": BRIDGE_KEY} + if USE_HOST_OLLAMA: + auth_headers["x-graph-debug-timings"] = "true" + doc_id = f"e2e-doc-{uuid.uuid4().hex[:8]}" + marker = f"E2E-MARKER-{uuid.uuid4().hex[:8]}" + + upsert = http_json( + "POST", + f"http://127.0.0.1:{BRIDGE_PORT}/documents/upsert", + { + "document_id": doc_id, + "content": f"Project Astra bridge E2E document with marker {marker}.", + "metadata": {"source": "codex-e2e"}, + "user_id": "codex", + "project_id": "project-astra-e2e", + "finalize": False, + }, + headers=auth_headers, + timeout=REQUEST_TIMEOUT, + ) + assert upsert["ok"] is True + + query = http_json( + "POST", + f"http://127.0.0.1:{BRIDGE_PORT}/query", + { + "query": f"What marker is in {doc_id}?", + "user_id": "codex", + "project_id": "project-astra-e2e", + "k": 3, + "return_context": True, + }, + headers=auth_headers, + timeout=REQUEST_TIMEOUT, + ) + assert query["ok"] is True + assert query["scope_enforced"] is True + assert marker in json.dumps(query) + + delete = http_json( + "POST", + f"http://127.0.0.1:{BRIDGE_PORT}/documents/delete", + { + "document_id": doc_id, + "finalize": False, + }, + headers=auth_headers, + timeout=REQUEST_TIMEOUT, + ) + assert delete["ok"] is True + return 0 + except Exception: + try: + logs = run(["docker", "logs", "--tail", "200", BRIDGE], check=False) + sys.stderr.write(logs.stdout) + sys.stderr.write(logs.stderr) + except Exception: + pass + raise + finally: + cleanup() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/openmemory-graphrag-bridge/tests/test_server_logic.py b/tools/openmemory-graphrag-bridge/tests/test_server_logic.py new file mode 100644 index 00000000..21c25913 --- /dev/null +++ b/tools/openmemory-graphrag-bridge/tests/test_server_logic.py @@ -0,0 +1,305 @@ +from __future__ import annotations + +import unittest +from pathlib import Path +import sys +import tempfile +import warnings +import logging +from unittest.mock import patch + +warnings.filterwarnings( + "ignore", + message=r".*PydanticDeprecatedSince20.*", +) +try: + from pydantic.warnings import PydanticDeprecatedSince20 +except Exception: # pragma: no cover - compatibility fallback + PydanticDeprecatedSince20 = None +else: + warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20) + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import server + + +class FakeResult: + def __init__(self, result_set): + self.result_set = result_set + + +class FakeGraphStore: + def __init__(self, rows=None): + self.rows = rows or [] + self.calls = [] + + async def query_raw(self, query, params=None): + self.calls.append((query, params)) + if "MATCH (d:Document)-[:PART_OF]->(c:Chunk)" in query and "RETURN d.id AS document_id, c.text AS chunk_text" in query: + return FakeResult(self.rows) + if "MATCH (d:Document {id: $document_id})" in query and "SET d.openmemory_document_id" in query: + return FakeResult([]) + if "MATCH (d:Document {id: id})" in query and "RETURN d.id AS id" in query: + ids = params["ids"] + return FakeResult([[doc_id, "codex", "D:/BooksDocs/Project Astra"] for doc_id in ids]) + raise AssertionError(f"Unexpected query: {query}") + + +class FakeRag: + def __init__(self, graph_store): + self._graph_store = graph_store + + +class FakeHeaders: + def __init__(self, warnings_list): + self._warnings_list = warnings_list + + def get_list(self, key): + if key == "X-HF-Warning": + return list(self._warnings_list) + return [] + + +class FakeResponse: + def __init__(self, warnings_list): + self.headers = FakeHeaders(warnings_list) + + +class ServerLogicTests(unittest.IsolatedAsyncioTestCase): + def test_parse_scope_from_chunk_text(self): + chunk = ( + "OpenMemory document id: abc\n" + "OpenMemory user id: codex\n" + "OpenMemory project id: D:/BooksDocs/Project Astra\n" + "OpenMemory source: openmemory:/memory/add\n\n" + "payload" + ) + parsed = server._parse_scope_from_chunk_text(chunk) + self.assertEqual(parsed["user_id"], "codex") + self.assertEqual(parsed["project_id"], "D:/BooksDocs/Project Astra") + self.assertEqual(parsed["source"], "openmemory:/memory/add") + + async def test_backfill_scope_metadata_dry_run(self): + rows = [ + [ + "doc-1", + "OpenMemory document id: doc-1\n" + "OpenMemory user id: codex\n" + "OpenMemory project id: D:/BooksDocs/Project Astra\n" + "OpenMemory source: openmemory:/memory/add\n\nbody", + ], + [ + "doc-2", + "OpenMemory document id: doc-2\n" + "OpenMemory user id: codex\n" + "OpenMemory project id: D:/BooksDocs/Project Astra\n" + "OpenMemory source: openmemory:/api/ide/events\n\nbody", + ], + ] + graph_store = FakeGraphStore(rows=rows) + rag = FakeRag(graph_store) + result = await server._backfill_scope_metadata(rag, dry_run=True, limit=1000) + self.assertTrue(result["ok"]) + self.assertEqual(result["scanned_documents"], 2) + self.assertEqual(result["backfilled_documents"], 2) + self.assertEqual(result["skipped_documents"], 0) + self.assertEqual(result["document_ids"], ["doc-1", "doc-2"]) + set_calls = [query for query, _ in graph_store.calls if "SET d.openmemory_document_id" in query] + self.assertEqual(set_calls, []) + + async def test_backfill_scope_metadata_apply(self): + rows = [ + [ + "doc-1", + "OpenMemory document id: doc-1\n" + "OpenMemory user id: codex\n" + "OpenMemory project id: D:/BooksDocs/Project Astra\n" + "OpenMemory source: openmemory:/memory/add\n\nbody", + ] + ] + graph_store = FakeGraphStore(rows=rows) + rag = FakeRag(graph_store) + result = await server._backfill_scope_metadata(rag, dry_run=False, limit=1000) + self.assertTrue(result["ok"]) + self.assertEqual(result["backfilled_documents"], 1) + set_calls = [query for query, _ in graph_store.calls if "SET d.openmemory_document_id" in query] + self.assertEqual(len(set_calls), 1) + + def test_merge_scoped_retriever_results_prefers_graph_native_mode(self): + item_type = server._load_graphrag()[-1] + result_type = server._load_graphrag()[-2] + primary = result_type( + items=[ + item_type( + content="## Source Document Passages\n[Source: a]\nalpha", + metadata={"section": "passages", "scope_filter_mode": "graph_native_chunk_pushdown"}, + ) + ], + metadata={"scope_candidate_chunk_count": 1}, + ) + secondary = result_type( + items=[ + item_type( + content="## Source Document Passages\n[Source: a]\nalpha\n---\n[Source: b]\nbeta", + metadata={"section": "passages", "scope_filter_mode": "graph_metadata_with_legacy_fallback"}, + ) + ], + metadata={"scope_blocks_graph_matched": 0, "scope_blocks_legacy_matched": 1}, + ) + merged = server._merge_scoped_retriever_results( + primary, + secondary, + RetrieverResult=result_type, + RetrieverResultItem=item_type, + ) + self.assertEqual(merged.metadata["scope_retrieval_mode"], "graph_native_pushdown_with_legacy_backfill") + self.assertEqual(merged.metadata["scope_candidate_chunk_count"], 1) + self.assertEqual(merged.metadata["scope_blocks_graph_matched"], 1) + self.assertEqual(merged.metadata["scope_blocks_legacy_matched"], 1) + + def test_finalize_graph_native_retriever_result_sets_graph_native_metadata(self): + item_type = server._load_graphrag()[-1] + result_type = server._load_graphrag()[-2] + primary = result_type( + items=[ + item_type( + content="## Source Document Passages\n[Source: a]\nalpha\n---\n[Source: b]\nbeta", + metadata={"section": "passages", "scope_filter_mode": "graph_native_chunk_pushdown"}, + ) + ], + metadata={"scope_candidate_chunk_count": 2}, + ) + finalized = server._finalize_graph_native_retriever_result( + primary, + RetrieverResult=result_type, + RetrieverResultItem=item_type, + ) + self.assertEqual(finalized.metadata["scope_retrieval_mode"], "graph_native_chunk_pushdown") + self.assertEqual(finalized.metadata["scope_blocks_graph_matched"], 2) + self.assertEqual(finalized.metadata["scope_blocks_legacy_matched"], 0) + self.assertEqual(finalized.metadata["scope_candidate_chunk_count"], 2) + + def test_finalize_graph_native_retriever_result_handles_empty(self): + result_type = server._load_graphrag()[-2] + finalized = server._finalize_graph_native_retriever_result( + None, + RetrieverResult=result_type, + RetrieverResultItem=server._load_graphrag()[-1], + ) + self.assertEqual(finalized.metadata["scope_retrieval_mode"], "none") + self.assertEqual(finalized.metadata["scope_blocks_graph_matched"], 0) + self.assertEqual(finalized.metadata["scope_blocks_legacy_matched"], 0) + + def test_update_scope_state_and_contract(self): + server._update_scope_state( + { + "documents_total": 11, + "documents_missing_scope": 0, + "chunks_total": 11, + "chunks_missing_scope": 0, + } + ) + self.assertFalse(server._scope_legacy_backfill_required_state) + + def test_scope_health_contract_fields_for_healthy_graph(self): + contract = server._scope_health_contract_fields(False) + self.assertEqual(contract["scope_storage_contract"], "document_chunk_properties") + self.assertTrue(contract["scope_operator_recovery_path_present"]) + self.assertTrue(contract["scope_storage_compatibility_path_present"]) + + def test_scope_health_contract_fields_for_stale_graph(self): + contract = server._scope_health_contract_fields(True) + self.assertEqual( + contract["scope_storage_contract"], + "document_chunk_properties_with_legacy_fallback", + ) + self.assertTrue(contract["scope_operator_recovery_path_present"]) + self.assertTrue(contract["scope_storage_compatibility_path_present"]) + + def test_legacy_scope_recovery_flag_defaults_false(self): + with patch.dict("os.environ", {}, clear=False): + self.assertFalse(server._legacy_scope_recovery_enabled()) + + def test_legacy_scope_recovery_flag_true(self): + with patch.dict("os.environ", {"OM_GRAPHRAG_ENABLE_LEGACY_SCOPE_RECOVERY": "true"}, clear=False): + self.assertTrue(server._legacy_scope_recovery_enabled()) + + def test_gliner_online_fallback_flag_defaults_false(self): + with patch.dict("os.environ", {}, clear=False): + self.assertFalse(server._gliner_online_fallback_enabled()) + + def test_gliner_online_fallback_flag_true(self): + with patch.dict("os.environ", {"OM_GRAPHRAG_GLINER_ALLOW_ONLINE_FALLBACK": "true"}, clear=False): + self.assertTrue(server._gliner_online_fallback_enabled()) + + def test_test_disable_extraction_flag_defaults_false(self): + with patch.dict("os.environ", {}, clear=False): + self.assertFalse(server._test_disable_extraction()) + self.assertIsNone(server._test_extractor_override()) + + async def test_test_extractor_override_returns_empty_graph(self): + with patch.dict("os.environ", {"OM_GRAPHRAG_TEST_DISABLE_EXTRACTION": "true"}, clear=False): + extractor = server._test_extractor_override() + self.assertIsNotNone(extractor) + graph = await extractor.extract(None, None, None) + self.assertEqual(graph.nodes, []) + self.assertEqual(graph.relationships, []) + self.assertEqual(graph.mentions, []) + + def test_gliner_cache_snapshot_path_prefers_ref(self): + with tempfile.TemporaryDirectory() as tmp: + cache = Path(tmp) + ref = cache / "models--urchade--gliner_medium-v2.1" / "refs" + snap = cache / "models--urchade--gliner_medium-v2.1" / "snapshots" / "abc123" + ref.mkdir(parents=True) + snap.mkdir(parents=True) + (ref / "main").write_text("abc123", encoding="utf-8") + resolved = server._gliner_cache_snapshot_path("urchade/gliner_medium-v2.1", str(cache)) + self.assertEqual(Path(resolved), snap) + + def test_hf_warning_filter_suppresses_only_target_message(self): + import huggingface_hub.utils._http as hf_http + + messages = [] + original_warned = set(hf_http._WARNED_TOPICS) + original_filters = list(hf_http.logger.filters) + original_level = hf_http.logger.level + original_propagate = hf_http.logger.propagate + + class _ListHandler(logging.Handler): + def emit(self, record): + messages.append(record.getMessage()) + + handler = _ListHandler() + try: + hf_http.logger.handlers = [] + hf_http.logger.addHandler(handler) + hf_http.logger.setLevel(logging.WARNING) + hf_http.logger.propagate = False + hf_http.logger.filters = [] + hf_http._WARNED_TOPICS.clear() + server._hf_warning_filter_patch_installed = False + server._hf_warning_logger_filter = None + original = hf_http._warn_on_warning_headers + server._install_hf_warning_filter_patch() + response = FakeResponse( + [ + "topic-a; Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN", + "topic-b; keep this warning", + ] + ) + hf_http._warn_on_warning_headers(response) + self.assertEqual(messages, ["keep this warning"]) + self.assertIs(hf_http._warn_on_warning_headers, original) + finally: + hf_http.logger.removeHandler(handler) + hf_http.logger.filters = original_filters + hf_http.logger.setLevel(original_level) + hf_http.logger.propagate = original_propagate + hf_http._WARNED_TOPICS.clear() + hf_http._WARNED_TOPICS.update(original_warned) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/sync-openmemory-models.mjs b/tools/sync-openmemory-models.mjs new file mode 100644 index 00000000..baaa6a3e --- /dev/null +++ b/tools/sync-openmemory-models.mjs @@ -0,0 +1,41 @@ +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, ".."); +const sourcePath = resolve(repoRoot, "models.yml"); +const mirrorPath = resolve(repoRoot, "packages", "openmemory-js", "models.yml"); +const checkOnly = process.argv.includes("--check"); + +if (!existsSync(sourcePath)) { + console.error(`[MODELS] source file not found: ${sourcePath}`); + process.exit(1); +} + +const source = readFileSync(sourcePath, "utf8"); +const mirror = existsSync(mirrorPath) ? readFileSync(mirrorPath, "utf8") : null; + +if (checkOnly) { + if (mirror !== source) { + console.error( + `[MODELS] package mirror is out of sync with source of truth: ${sourcePath}`, + ); + console.error(`[MODELS] mirror path: ${mirrorPath}`); + console.error( + `[MODELS] run: node tools/sync-openmemory-models.mjs`, + ); + process.exit(1); + } + + console.log("[MODELS] package mirror is in sync"); + process.exit(0); +} + +if (mirror === source) { + console.log("[MODELS] package mirror already up to date"); + process.exit(0); +} + +writeFileSync(mirrorPath, source, "utf8"); +console.log(`[MODELS] synced ${mirrorPath} from ${sourcePath}`);