Skill: qsmtco/curated-search (version 1.0.7)

--- name: curated-search description: Domain-restricted full-text search over curated technical documentation version: 1.0.7 author: qsmtco license: MIT homepage: https://github.com/openclaw/curated-search metadata: openclaw: requires: bins: ["node"] emoji: "🔍" --- # Curated Search Skill ## Summary Domain-restricted full-text search over a curated whitelist of technical documentation (MDN, Python docs, etc.). Provides clean, authoritative results without web spam. ## External Endpoints This skill does **not** call any external network endpoints during search operations. The crawler optionally makes outbound HTTP requests during index builds (one‑time setup), but those are user‑initiated (`npm run crawl`) and respect the configured domain whitelist. ## Security & Privacy - **Search is fully local** – After the index is built, all queries run offline; no data leaves your machine. - **Crawling is optional and whitelist‑scoped** – The crawler only accesses domains you explicitly list in `config.yaml`. It respects `robots.txt` and configurable delays. - **No telemetry** – No usage data is transmitted externally. - **Configuration** is read from local `config.yaml` and the index file in `data/`. ## Model Invocation Note The `curated-search.search` tool is invoked **only when the user explicitly calls it**. It does not run autonomously. OpenClaw calls the tool handler (`scripts/search.js`) when the user asks to search the curated index. ## Trust Statement By using this skill, you trust that the code operates locally and only crawls domains you approve. The skill does not send your queries or workspace data to any third party. Review the open‑source implementation before installing. --- ## Tool: curated-search.search Search the curated index. ### Parameters | Name | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `query` | string | yes | — | Search query terms | | `limit` | number | no | 5 | Maximum results (capped by config.max_limit, typically 100) | | `domain` | string | no | null | Filter to specific domain (e.g., `docs.python.org`) | | `min_score` | number | no | 0.0 | Minimum relevance score (0.0–1.0); filters out low-quality matches | | `offset` | number | no | 0 | Pagination offset (skip first N results) | ### Response JSON array of result objects: ```json [ { "title": "Python Tutorial", "url": "https://docs.python.org/3/tutorial/", "snippet": "Python is an easy to learn, powerful programming language...", "domain": "docs.python.org", "score": 0.87, "crawled_at": 1707712345678 } ] ``` **Fields:** - `title` — Document title (cleaned) - `url` — Source URL (canonical) - `snippet` — Excerpt (~200 chars) from content - `domain` — Hostname of source - `score` — BM25 relevance score (higher is better; not normalized 0–1 but typically 0–1 range) - `crawled_at` — Unix timestamp when page was crawled ### Example Agent Calls ``` search CuratedSearch for "python tutorial" search CuratedSearch for "async await" limit=3 domain=developer.mozilla.org search CuratedSearch for "linux man page" min_score=0.3 ``` ### Errors If an error occurs, the tool exits non-zero and prints a JSON error object to stderr, e.g.: ```json { "error": "index_not_found", "message": "Search index not found. The index has not been built yet.", "suggestion": "Run the crawler first: npm run crawl", "details": { "path": "data/index.json" } } ``` Common error codes: | Code | Meaning | Suggested Fix | |------|---------|---------------| | `config_missing` | Configuration file not found | Specify `--config` path or ensure config.yaml exists | | `config_invalid` | YAML parsing failed | Check syntax in config.yaml | | `config_missing_index_path` | `index.path` not set | Add index.path to config | | `index_not_found` | Index file missing | Run `npm run crawl` to build index | | `index_corrupted` | Index file incompatible or corrupted | Rebuild index with `npm run crawl` | | `index_init_failed` | Unexpected index initialization error | Check permissions, reinstall dependencies | | `missing_query` | No query provided | Provide `--query` argument | | `query_too_long` | Query exceeds 1000 characters | Shorten the query | | `limit_exceeded` | Limit > config.max_limit | Use a smaller limit | | `invalid_domain` | Domain filter malformed | Use format like `docs.python.org` | | `conflicting_flags` | Mutually exclusive flags used (e.g., `--stats` with `--query`) | Use flags correctly | | `stats_failed` | Could not retrieve index stats | Ensure index is accessible | | `search_failed` | Search execution threw an error | Check query and index integrity | ## Configuration Edit `config.yaml` in the skill directory. Key sections: - `domains` — whitelist of allowed domains (required) - `seeds` — starting URLs for crawling - `crawl` — depth, delay, timeout, max_documents - `content` — min_content_length, max_content_length - `index` — path to index files - `search` — default_limit, max_limit, min_score See `README.md` for full configuration docs. ## Support - Full documentation: `README.md` - Technical specs: `specs/` - Build plan: `PLAN.md` - Contributor guide: `CONTRIBUTING.md` - Issues: Report on GitHub (or via OpenClaw maintainers)

path: CHANGELOG.md
content: # Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.org/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [1.0.7] - 2026-02-18

### Fixed

- **Security manifests:** Added security manifest headers to all executable scripts (`scripts/search.js`, `scripts/crawl.js`, `scripts/validate-config.js`, `scripts/health-check.js`, `scripts/curated_search`, `scripts/publish-clean.sh`) declaring environment variables, external endpoints, and file access patterns
- **Compliance:** Updated to meet ClawHub security guidelines for script documentation

---

## [1.0.6] - 2026-02-18

### Fixed

- **Publishing compliance:** Added required sections to `SKILL.md` (External Endpoints, Security & Privacy, Model Invocation Note, Trust Statement)
- **Updated frontmatter:** Added `version`, `author`, `license`, `homepage`, and `emoji` to meet ClawHub manifest standards
- **Removed conflicting manifest:** Deleted `skill.json` to avoid duplicate manifest issues; `SKILL.md` is now the sole source of truth
- **Improved .clawhubignore:** Excluded root-level test files (`test*.js`, `test-indexer.js`, `test-mini.js`) and `jest.config.js` to prevent “suspicious” flags from unused code in the bundle
- **Removed unused test artifacts** from packaging

---

## [1.0.5] - 2026-02-14

### Security
- Added `metadata.openclaw.requires.bins: ["node"]` to SKILL.md to declare Node.js runtime requirement
- This resolves ClawHub security scan "suspicious" flag due to missing required binaries declaration

## [1.0.4] - 2026-02-14

### Security
- **Removed legacy network server component** (`src/search-api.js`) that was never used but present in repo, reducing attack surface
- Updated README.md to remove legacy references
- Improved `.clawhubignore` to exclude internal audit files (`*AUDIT*.md`, `*CRITICAL*.md`, `SYS_*`, `YACY_*`, etc.)
- No functional changes to the search tool

## [1.0.3] - 2026-02-14

### Security
- Fixed accidental inclusion of internal audit documents in published package
- Updated `.clawhubignore` to exclude `*AUDIT*.md`, `*CRITICAL*.md`, `SYS_*`, `YACY_*`, and related internal notes
- No functional changes to the skill itself

## [1.0.0] - 2025-02-12

### Added
- First public release of Curated Search
- Domain-restricted crawling with curated whitelist
- Full-text search over MiniSearch index
- OpenClaw native integration
- Configurable crawl parameters (depth, delay, timeout)
- Content extraction with site-specific selectors
- Robots.txt compliance and rate limiting
- State checkpointing and resume
- Health monitoring and graceful shutdown
- Jest test suite with coverage targets
- Comprehensive documentation (README, SKILL, deployment)

---

**Note:** This changelog starts with the initial release (1.0.0). Future updates will list changes under `[Unreleased]` and move versions downward.


path: CONTRIBUTING.md
content: # Contributing to Curated Search

Thank you for your interest in improving Curated Search! This document provides guidelines and instructions for contributing.

## Areas of Contribution

### 1. Adding New Domains

The curated whitelist is the core product. When adding a new domain:

**a. Evaluate Quality & Authority**
- Prefer official documentation (e.g., `docs.python.org`, `developer.mozilla.org`)
- Avoid marketing-heavy homepages; target specific `/docs/`, `/api/` sections
- Consider maintenance burden: stable sites are better than ones that change structure frequently

**b. Update `config.yaml`**
- Add the domain to the `domains:` list
- Add one or more seed URLs under `seeds:` that point directly to content-rich pages

**c. Add Site-Specific Content Extraction (if needed)**
If the domain requires custom selectors or cleanup:
- Open `src/content-extractor.js`
- Add the domain to `detectContentType` mapping
- Implement extraction in the appropriate method (`extractMDN`, `extractGeneric`, or new dedicated method)
- Add corresponding test fixtures in `test/fixtures/`

**d. Document Rationale**
Update `DOMAIN_GUIDE.md` with a new entry explaining:
- What the domain provides
- Why it's valuable
- Any special crawl considerations (rate limits, blocking, depth)

**e. Test**
```bash
# Create a small config with just the new domain and shallow depth
npm run crawl
# Verify index contains documents and search works
node scripts/search.js --query="test" --pretty
```

### 2. Code Style & Testing

**Style:**
- Use consistent indentation (2 spaces)
- Prefer explicit variable names
- Add JSDoc comments for public functions
- Keep functions small and focused

**Testing:**
- All new functionality must include unit tests
- Update existing tests when modifying behavior
- Aim to maintain or increase coverage
- Use fixtures in `test/fixtures/` for HTML content extraction tests

Run tests frequently:
```bash
npm test
```

### 3. Pull Request Process

1. Fork the repository and create a feature branch
2. Make your changes with clear, concise commit messages
3. Ensure all tests pass (`npm test`)
4. Update documentation (README, SKILL.md, DOMAIN_GUIDE.md) as needed
5. Submit a pull request with a description of the changes and rationale
6. Address any review feedback

### 4. Reporting Issues

When filing issues, include:
- A clear description of the problem
- Steps to reproduce (including config snippets, sample URLs)
- Expected vs actual behavior
- Relevant logs or error output
- Environment details (OS, Node version)

## Development Setup

```bash
# Clone and install dependencies
npm ci

# Run tests
npm test

# Run a small crawl for manual testing (edit config.yaml first)
npm run crawl

# Search the index
node scripts/search.js --query="your query"
```

## Key Files

- `src/crawler.js` — main crawler orchestration
- `src/content-extractor.js` — HTML → text pipeline
- `src/indexer.js` — MiniSearch wrapper
- `src/url-normalizer.js` — canonicalization and SSRF prevention
- `src/rate-limiter.js` — per-host politeness
- `scripts/search.js` — CLI tool
- `config.yaml` — default configuration

## Philosophy

- The whitelist is a feature, not a limitation. Quality over quantity.
- Respect target sites: obey robots.txt, use reasonable delays, and don't hammer servers.
- Keep the implementation simple and maintainable.
- Document decisions and assumptions.

Questions? Open an issue or reach out.


path: DOMAIN_GUIDE.md
content: # Curated Search - Domain Guide

**Purpose:** Documenting why each domain is whitelisted, what content it provides, and how to crawl it effectively.

**Last Updated:** 2026-02-12

---

## Domain Categories

### Programming Languages & Runtimes

#### Python (python.org)
**What:** Official Python documentation and resources
**Why:** Definitive source for Python standard library, language reference, tutorials
**Priority:** Critical
**Crawl Strategy:**
- Seed: `/doc/` paths
- Depth: 3 (extensive docs hierarchy)
- Note: Well-structured, crawler-friendly

#### Node.js (nodejs.org)
**What:** Node.js runtime documentation
**Why:** JavaScript server-side runtime docs, API reference
**Priority:** High
**Crawl Strategy:**
- Seed: `/docs/` paths
- Depth: 2
- Note: Modern site structure, responsive

---

### Web Development

#### MDN Web Docs (developer.mozilla.org)
**What:** Mozilla Developer Network - HTML, CSS, JavaScript, Web APIs
**Why:** The gold standard for web platform documentation
**Priority:** Critical
**Crawl Strategy:**
- Seed: `/en-US/docs/Web/` (language-agnostic paths)
- Depth: 3
- Note: Massive site, focus on Web/API section
- Caution: Respect robots.txt, very popular site

---

### System Administration & Linux

#### Linux man pages (man7.org)
**What:** Linux man pages, system calls, library functions
**Why:** Authoritative Linux C API and system documentation
**Priority:** High
**Crawl Strategy:**
- Seed: `/linux/man-pages/` paths
- Depth: 2
- Note: Text-heavy, technical content

#### Linux.die.net
**What:** Alternative Linux man pages with search
**Why:** Backup/alternative for Linux docs
**Priority:** Medium
**Crawl Strategy:**
- Seed: `/man/` paths
- Depth: 2
- Note: May overlap with man7.org, dedupe by URL

---

### Q&A and Community Knowledge

#### Stack Overflow (stackoverflow.com)
**What:** Programming Q&A community
**Why:** Millions of practical solutions, code examples
**Priority:** High (but tricky)
**Crawl Strategy:**
- Seed: `/questions/tagged/` specific tags
- Depth: 1 (question pages have related links)
- Note: Aggressive anti-scraping, may require careful throttling
- Alternative: Consider StackPrinter links for better crawling
**Caution:** High load site, very sensitive to crawling

---

### Reference & Encyclopedia

#### Wikipedia (en.wikipedia.org)
**What:** Collaborative encyclopedia, technical topics
**Why:** Broad technical overview, theory, concepts
**Priority:** Medium
**Crawl Strategy:**
- Seed: `/wiki/` pages for specific tech topics
- Depth: 2
- Note: Focus on Computing/Technology categories
- Caution: Very large, use depth limits strictly

---

### Platforms & Code Repositories

#### GitHub (github.com)
**What:** Code repositories, READMEs, documentation
**Why:** Actual code, real-world examples, library docs
**Priority:** Medium-High
**Crawl Strategy:**
- Seed: Specific repo paths, not root
- Depth: 1 (follow links within repos)
- Note: Most content is dynamic, focus on README.md files
- Alternative: Use raw.githubusercontent.com for markdown files
**Caution:** Rate limits apply, respect API guidelines

---

### Self-Documented

#### OpenClaw Docs (docs.openclaw.ai)
**What:** OpenClaw documentation
**Why:** Self-documenting the system
**Priority:** Low (dogfooding)
**Crawl Strategy:**
- Seed: Root and navigation pages
- Depth: 2
**Note:** Validate during testing

---

### Commercial (Optional/Experimental)

#### Amazon (amazon.com, amzn.to)
**What:** Product pages, reviews
**Why:** Hardware specs, technology product info
**Priority:** Low
**Crawl Strategy:**
- Seed: `/s?k=technology+keywords` search results
- Depth: 1
- Note: Heavy anti-scraping, may not be feasible
**Caution:** Consider removing if blocked consistently

---

## Domain Risk Assessment

| Domain | Blocking Risk | Content Quality | Maintenance Burden |
|--------|---------------|-----------------|-------------------|
| python.org | Low | High | Low |
| nodejs.org | Low | High | Low |
| developer.mozilla.org | Medium | Critical | Low |
| man7.org | Low | High | Low |
| stackoverflow.com | High | High | Medium |
| github.com | Medium | High | Medium |
| wikipedia.org | Low | Medium | Low |
| amazon.com | High | Low | High |

**Recommendation:** Start with Low/Medium risk domains for MVP. Add high-risk ones later with fallback strategies.

---

## Seed URL Strategy

**Don't crawl homepages.** They're usually:
- Marketing content (not searchable docs)
- Navigation-heavy (lots of links, little text)
- Rate-limited or monitored for abuse

**Instead, target:**

| Domain Type | Good Seed URL Pattern | Example |
|-------------|----------------------|---------|
| Documentation | `/docs/`, `/doc/`, `/guide/` | `https://docs.python.org/3/` |
| API Reference | `/api/`, `/reference/` | `https://nodejs.org/api/` |
| Wiki-style | `/wiki/` prefix | `https://en.wikipedia.org/wiki/Python_(programming_language)` |
| Knowledge Base | `/kb/`, `/help/` | `https://help.github.com/` |
| Questions | `/questions/`, `/q/` | `https://stackoverflow.com/questions/tagged/python` |

---

## Fallback Strategies

**When a domain blocks crawling:**

1. **Try alternative paths:**
   - `docs.example.com` instead of `example.com/docs`
   - `developer.example.com` for API docs
   - `support.example.com` for KB articles

2. **Use mirror sites:**
   - MDN: Multiple language mirrors available
   - Man pages: Multiple hosting sites

3. **Stack Overflow alternatives:**
   - Use StackPrinter URLs (text-only version)
   - Example: `http://stackprinter.appspot.com/export?question=123&service=stackoverflow&language=en&hideAnswers=false&width=640`

4. **GitHub alternatives:**
   - Raw content: `raw.githubusercontent.com`
   - GitHub Pages: `username.github.io/repo`

5. **Accept defeat:**
   - If a site consistently blocks with 403/429
   - Document in DOMAIN_GUIDE.md
   - Consider removing from whitelist

---

## Content Deduplication

**Challenge:** Same content appears on multiple domains

**Examples:**
- Man pages: man7.org vs linux.die.net
- Python docs: docs.python.org vs multiple mirrors
- Wikipedia: Language-specific vs mobile versions

**Strategy:**
- Normalize URLs (strip mobile prefixes)
- Hash content and skip duplicates
- Prefer canonical URLs (rel="canonical")
- Index only one authoritative source

---

## Updating This Guide

**When adding a new domain:**
1. Add to appropriate category section
2. Document rationale (what, why, priority)
3. Add crawl strategy notes
4. Update risk assessment table
5. Test seed URLs before enabling in production

**When a domain stops working:**
1. Document failure mode (403, 429, timeout)
2. Try fallback strategies
3. If consistently failing, consider removal
4. Update risk assessment

---

**Rule:** When in doubt, prefer quality over quantity. Ten well-indexed domains beat a hundred poorly scraped ones.


path: IMPLEMENTATION_SUMMARY_2026-02-12.md
content: # Implementation Summary: Critical Gaps Addressed

**Date:** 2026-02-12  
**Scope:** Content Extraction (3.2), URL Normalization (3.1), Rate Limiting (3.3)  
**Status:** Core functionality implemented and integrated

---

## Overview

Addressed the three critical gaps identified in `SYS_AUDIT_CRITICAL.md`:

1. **Content Extraction** — built full pipeline with type detection, boilerplate removal, code preservation, quality validation
2. **URL Normalization** — canonicalization with tracking param stripping, trailing slash removal, SSRF prevention
3. **Rate Limiter** — per-host delay with robots.txt awareness

All components integrated into `src/crawler.js`. Indexer updated to accept precomputed excerpts.

---

## Files Created

| File | Purpose |
|------|---------|
| `src/content-extractor.js` | Full content extraction pipeline per Phase 3.2 spec |
| `src/url-normalizer.js` | URL canonicalization + SSRF prevention (Phase 3.1) |
| `src/rate-limiter.js` | Per-host rate limiting (Phase 3.3) |

---

## Files Modified

| File | Changes |
|------|---------|
| `src/crawler.js` | - Added ContentExtractor, urlNormalizer, RateLimiter imports<br>- Constructor: `this.extractor`, `this.rateLimiter`<br>- Main loop: replaced global delay with `await this.rateLimiter.waitForSlot(hostname)`<br>- `enqueue`: now normalizes URL, checks SSRF, uses `urlNormalizer.isWhitelisted`<br>- `parseHtml`: uses ContentExtractor, preserves `excerpt`, `quality`, `type`<br>- Added `getCrawlDelay` method for robots.txt delay<br>- Removed obsolete `isAllowedDomain` method |
| `src/indexer.js` | `addDocument`: use provided `doc.excerpt` if present, else generate |
| `package.json` | Added `"marked": "^12.0.0"` dependency for GitHub raw markdown |
| `skill.yaml` | Previously updated to Phase 5 spec (5 parameters) — unchanged |

---

## Implementation Details

### Content Extractor (`src/content-extractor.js`)

**Pipeline:**
1. Detect type (MDN, Python docs, Wikipedia, GitHub HTML/raw, StackPrinter, manpage, generic)
2. Remove boilerplate (nav, footer, aside, ads, cookie banners, etc.)
3. Extract title (priority: `<title>`, `<h1>`, og:title, twitter:title, `<h2>`, first sentence)
4. Extract content by type with site-specific cleanup
5. Normalize whitespace; preserve code blocks (`<pre><code>` → ` ``` `, `<code>` → `` ` ``)
6. Generate excerpt (first 200 chars, sentence boundary, code blocks stripped)
7. Validate quality: min 500 chars, error page patterns, stub patterns

**Site-specific strategies:**
- **MDN:** Remove `aside.interactive`, `.document-toc`
- **Wikipedia:** Remove `.infobox`, `.navbox`, `#See_also` section
- **GitHub raw:** Use `marked` to parse markdown → HTML → text
- **StackPrinter:** Return full body text (could parse Q&A later)
- **Man pages:** Extract from `<pre>`, strip headers/footers, parse NAME for title

**Fallbacks:**
- Generic selectors → text density scoring → body with boilerplate removal

### URL Normalizer (`src/url-normalizer.js`)

**Normalization steps:**
- Allow only `http:`/`https:`
- Lowercase hostname; strip trailing dot
- Remove default ports (`:80`, `:443`)
- Remove trailing slash (except root)
- Strip tracking params: `utm_*`, `fbclid`, `gclid`, `ref`, `_ga`, `_gid`, etc.
- Remove fragment
- Returns `null` for invalid URLs

**SSRF prevention:**
- `isBlockedHost` checks IP against private ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 0.0.0.0/8, IPv6 loopback/unique-local/link-local, 100.64.0.0/10, 192.0.0.0/24

**Whitelist check:** Suffix match (exact or subdomain)

### Rate Limiter (`src/rate-limiter.js`)

- Per-host buckets: `Map<hostname, {lastRequest}>`
- `waitForSlot(hostname)` enforces delay via setTimeout
- `getDelay(hostname)` returns `Math.max(config.delay, robotsCrawlDelay*1000)`
- `getCrawlDelay` delegated to crawler's robots manager (async)

**Integration:** Main loop now `await this.rateLimiter.waitForSlot(hostname)` before each fetch.

---

## Integration Notes

- Crawler's `enqueue` now expects raw URLs and normalizes internally; duplicates prevented by normalized form.
- SSRF check uses `urlNormalizer.isBlockedHost` on hostname after normalization.
- `parseHtml` returns structured object with `title`, `content`, `excerpt`, `quality`, `type`, `links`, etc.
- Indexer accepts optional `excerpt`; uses provided or generates from content.
- No global delay anymore; rate limiting is per-host and respects robots.txt.

---

## Testing Status

The test suite is comprehensive with 128 passing tests covering:
- Unit tests for indexer, URL normalizer, content extractor, rate limiter, crawler, search tool, config validation
- Integration tests for search tool and config validation
- All site-specific content extraction fixtures (7 types) are present and passing
- Robots.txt caching with TTL (24h) tested in politeness tests
- Rate limiter per-host delay and robots delay integration tested

All tests pass consistently. Coverage includes core functionality and edge cases.

---

## Known Gaps / Future Enhancements

- **Link density quality check** — not implemented (would require original element structure before text conversion). Low priority.
- **Index hash consistency check on resume** — optional; currently not performed.
- **Content hash deduplication** — future improvement (spec notes it's optional for MVP).
- **RateLimiter metrics exposure** — could add counters/observability for monitoring.
- **Documentation** — CONTRIBUTING.md not yet created (per Phase 7 spec).
- **Skill error code reference** — SKILL.md could be expanded to list all possible error codes from search tool.

---

## How to Verify

```bash
# Install dependencies (including marked)
npm ci

# Create a config.yaml with small whitelist and seeds
# Then run:
npm run crawl
```

Observe logs for quality metrics, check `data/index.json` for content structure.

---

**End of summary**


path: PLAN.md
content: # Curated Search Skill - Build Plan

**Purpose:** Domain-restricted search system that indexes only trusted technical sources, providing clean, authoritative results for OpenClaw agents and human users.

**Core Philosophy:** The whitelist curation is the product. Keep implementation simple, ship fast, iterate based on usage.

---

## Phase 1: Foundation & Configuration

**Goal:** Lock in the domain whitelist and crawl parameters that define what we search.

1. **Curate Domain Whitelist**
   - Define authoritative sources (docs, repos, wikis, Q&A sites)
   - Categorize by topic (programming, sysadmin, web dev, etc.)
   - Document rationale for each domain

2. **Design Config Schema**
   - YAML structure for domains, crawl settings, index paths
   - Environment-specific overrides (dev vs prod)
   - Validation rules (required fields, value ranges)

3. **Seed URL Strategy**
   - Identify entry points for each domain (not just homepages)
   - Prioritize high-value paths (/docs, /api, /wiki)
   - Plan for domains that block crawlers (fallback strategies)

---

## Phase 2: Core Indexer (The Library)

**Goal:** Build the searchable document store with zero external dependencies.

1. **Implement Document Model**
   - Fields: url, title, content, excerpt, domain, crawled_at, depth
   - Compute excerpt (first 200 chars + ellipsis)
   - Validate URLs and normalize

2. **Integrate MiniSearch**
   - Configure index fields (title, content, domain)
   - Set up stored fields (url, domain, excerpt, crawled_at)
   - Design serialization format (JSON persistence)

3. **Build CRUD Operations**
   - `addDocument(doc)` - index new content
   - `removeByUrl(url)` - purge outdated entries
   - `search(query, opts)` - BM25 ranked results
   - `getStats()` - document count, domains, last crawl

4. **Persistence Layer**
   - Save index to JSON on changes
   - Load index on startup
   - Handle corruption gracefully (rebuild if needed)

---

## Phase 3: Domain-Restricted Crawler

**Goal:** Polite, focused crawler that respects boundaries and stays on whitelist.

1. **URL Discovery**
   - Extract links from HTML using JSDOM
   - Normalize URLs (resolve relative, strip fragments)
   - Filter: whitelist domains only, respect max depth

2. **Content Extraction**
   - Parse HTML to extract title and main text
   - Strip boilerplate (nav, footers, ads where possible)
   - Handle different content types (docs, READMEs, wikis)

3. **Politeness Controls**
   - robots.txt parsing and respect
   - Rate limiting per host (configurable delay)
   - User agent identification
   - Request timeout handling

4. **Orchestration**
   - Breadth-first crawling with depth tracking
   - Resume capability (save crawl state)
   - Incremental updates (only crawl changed content)
   - Graceful shutdown (finish current batch, save state)

---

## Phase 4: Search Tool Interface

**Goal:** Simple CLI tool that OpenClaw can invoke directly.

1. **Argument Parsing**
   - `--query="search terms"` (required)
   - `--limit=N` (default 5)
   - `--domain=example.com` (optional filter)

2. **Query Execution**
   - Load index from disk
   - Execute search with filters
   - Format results as JSON array

3. **Error Handling**
   - Empty index → return empty array (not error)
   - Missing config → clear error message
   - Malformed query → sanitize and attempt

4. **Performance**
   - Target: <500ms for index load + search
   - Optimize: cache index in memory for multiple calls? (future)

---

## Phase 5: OpenClaw Integration

**Goal:** Register as a native skill with proper metadata.

1. **Skill Manifest (skill.yaml)**
   - Tool definition with parameters
   - Command mapping to search script
   - Timeout and return type specifications

2. **Wrapper Scripts**
   - Ensure Node.js executable permissions
   - Handle path resolution relative to skill directory
   - Set working directory correctly

3. **Registration**
   - Copy/link skill to OpenClaw skills directory
   - Verify tool appears in `openclaw tools list`
   - Test invocation from agent

---

## Phase 6: Testing & Validation

**Goal:** Prove it works end-to-end before relying on it.

1. **Unit Tests**
   - Indexer: add, search, remove, persist
   - Crawler: URL filtering, robots.txt parsing
   - Config: validation, loading

2. **Smoke Tests**
   - Crawl single domain, shallow depth (depth=1)
   - Verify documents appear in index
   - Search returns expected results

3. **Integration Tests**
   - Full crawl of 2-3 domains
   - Query across different domains
   - Filter by domain works correctly
   - Empty queries handled gracefully

4. **Load Tests** (optional)
   - Index 10k documents, measure search latency
   - Verify memory usage stays bounded

---

## Phase 7: Documentation

**Goal:** Clear docs for setup, usage, and contribution.

1. **README.md** (public)
   - What this is and why it exists
   - Quick start (install, configure, run)
   - Architecture overview
   - Troubleshooting common issues

2. **SKILL.md** (OpenClaw reference)
   - Tool specification for agent
   - Example queries
   - Response format reference

3. **CONTRIBUTING.md** (future)
   - How to add new domains
   - Code style and testing
   - Submitting PRs

---

## Phase 8: Deployment & Operations

**Goal:** Run continuously without manual intervention.

1. **Initial Index Build**
   - Run full crawl of all whitelisted domains
   - Monitor for failures or blocks
   - Verify index size and search quality

2. **Update Strategy**
   - Schedule: Weekly re-crawls? Triggered updates?
   - Incremental vs full rebuild
   - Backup/snapshot index before updates

3. **Monitoring**
   - Log crawl success/failure rates
   - Track index size growth over time
   - Alert on crawl failures or search errors

4. **Maintenance**
   - Remove dead domains from whitelist
   - Add new authoritative sources
   - Prune old/outdated documents

---

## Future Enhancements (Post-MVP)

**Only after core system is stable and in daily use:**

1. **Semantic Search**
   - Vector embeddings (sentence-transformers)
   - Hybrid keyword + semantic ranking
   - Requires: embedding model, vector DB

2. **Web UI**
   - Simple search interface for humans
   - Results with previews and snippets

3. **Federation**
   - Share curated indexes between trusted OpenClaw instances
   - Index exchange format
   - Distributed crawling (avoid hammering same sites)

4. **Advanced Crawling**
   - Sitemap.xml parsing
   - RSS/Atom feed monitoring for updates
   - API-based content (GitHub API, etc.)

5. **Performance Optimization**
   - Persistent API server (if load demands it)
   - In-memory index caching
   - Connection pooling for crawls

---

## Success Criteria

**MVP is complete when:**

- [ ] All whitelisted domains crawled successfully (depth=2)
- [ ] Index contains >1000 documents from >5 domains
- [ ] Search tool returns relevant results in <1 second
- [ ] OpenClaw agent can invoke tool and get useful answers
- [ ] Documentation is clear enough for another human to set up

**Quality metrics:**
- Search result relevance (subjective: "did I find what I needed?")
- Crawl completion rate (% of domains successfully indexed)
- System uptime (no crashes during normal operation)
- Maintenance burden (hours per week to keep running)

---

## File Locations

This plan: `skills/curated-search/PLAN.md`

Implementation will reside in: `skills/curated-search/`
- `config.yaml` - Domain whitelist and settings
- `src/indexer.js` - Search index library
- `src/crawler.js` - Domain-restricted crawler
- `scripts/search.js` - CLI tool for OpenClaw
- `skill.yaml` - OpenClaw skill manifest
- `README.md` - Public documentation
- `SKILL.md` - Agent reference

---

**Philosophy Check:**

- Complexity should match the problem. Don't build a search engine, build a curated index.
- The whitelist is a feature, not a limitation. Embrace it.
- Working beats perfect. Ship the MVP, improve incrementally.
- Document assumptions. The next person (or future you) needs context.


path: README.md
content: # Curated Search

A domain-restricted search skill for OpenClaw that indexes only trusted technical documentation. Replace generic web search with authoritative, spam-free results.

[![Tests](https://github.com/openclaw/curated-search/actions/workflows/test.yml/badge.svg)](https://github.com/openclaw/curated-search/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- **Whitelist-based crawling** — only trusted domains (MDN, Python docs, Node.js, etc.)
- **Respects robots.txt** — polite crawling with configurable delay
- **BM25 full-text search** — powered by MiniSearch (pure JavaScript, no DB)
- **No persistent server** — CLI tool invoked per query (simple, reliable)
- **Works offline** — after initial crawl, no external calls needed
- **OpenClaw native** — seamless agent integration

## Quick Start

```bash
# 1. Install dependencies
npm install

# 2. Edit config.yaml (optional — customize domains/ seeds)
# 3. Build the index (crawl whitelisted sites)
npm run crawl

# 4. Test search
node scripts/search.js --query="python tutorial" --limit=5
```

Expected output (JSON):
```json
[
  {
    "title": "Python Tutorial",
    "url": "https://docs.python.org/3/tutorial/",
    "snippet": "Python is an easy to learn...",
    "domain": "docs.python.org",
    "score": 0.87,
    "crawled_at": 1707712345678
  }
]
```

**Real example from a populated index:**
```json
{
  "query": "python",
  "total": 56,
  "results": [
    {
      "title": "What’s new in Python 3.14 — Python 3.14.3 documentation",
      "url": "https://docs.python.org/whatsnew/3.14.html",
      "domain": "docs.python.org",
      "snippet": "What’s new in Python 3.14¶ Editors: Adam Turner and Hugo van Kemenade This article explains the new features in Python 3.14...",
      "score": 8.809,
      "crawled_at": 1771044132217
    }
  ],
  "took_ms": 4,
  "limit": 5,
  "offset": 0
}
```

## OpenClaw Integration

1. Copy/link skill to OpenClaw skills directory:
   ```bash
   # Already in workspace: ~/.openclaw/workspace/skills/curated-search/
   # Ensure OpenClaw is configured to load workspace skills
   ```

2. Reload OpenClaw:
   ```bash
   openclaw gateway restart
   ```

3. Verify tool is discovered:
   ```bash
   openclaw tools list | grep curated
   # Should show: curated-search.search
   ```

4. Use in chat:
   ```
   User: search CuratedSearch for "python async await"
   Agent: [calls tool and returns results]
   ```

## Configuration

Edit `config.yaml` to customize behavior:

```yaml
# ====================
# DOMAIN WHITELIST
# ====================
# Only these domains will be crawled and indexed.
domains:
  - docs.python.org
  - developer.mozilla.org
  - nodejs.org
  - man7.org
  - en.wikipedia.org
  - github.com
  - docs.openclaw.ai
  # ... add more

# ====================
# SEED URLS
# ====================
# Starting points for crawling. Use specific docs pages not just homepages.
seeds:
  - "https://docs.python.org/3/tutorial/"
  - "https://developer.mozilla.org/en-US/docs/Web/JavaScript"
  - "https://nodejs.org/api/"

# ====================
# CRAWL SETTINGS
# ====================
crawl:
  depth: 2                  # 0 = seeds only, 1 = immediate links, 2 = recommended
  delay: 1000               # milliseconds between requests to same host (polite)
  timeout: 30               # HTTP request timeout (seconds)
  user_agent: "CuratedSearch/1.0 (OpenClaw Bot; +https://github.com/openclaw/openclaw)"
  max_documents: 10000      # stop after indexing this many pages
  concurrent_requests: 1    # keep 1 for politeness
  respect_robots: true      # obey robots.txt

# ====================
# SEARCH SETTINGS
# ====================
search:
  default_limit: 10
  max_limit: 100
  min_score: 0.0

# ====================
# LOGGING
# ====================
logging:
  level: "info"             # debug, info, warn, error
  file: "logs/curated-search.log"
  console: true
```

After editing `config.yaml`, the **search tool** picks up changes immediately. The **crawler** (`npm run crawl`) uses the config on next run.

## How It Works

```
OpenClaw Agent
    ↓ (tool call)
node scripts/search.js --query="python" --limit=5
    ↓
loads data/index.json (MiniSearch)
    ↓
BM25 search with optional filters (domain, minScore)
    ↓
returns JSON array of results
```

The crawler (`npm run crawl`) populates the index:

```
seeds → fetch → extract content → index → save → repeat until depth or max_documents
```

No server is running between queries — each tool invocation loads the index fresh (fast, <500ms on warm cache).

## Performance

- Index load (cold): ~200–500ms (10k docs)
- Search latency: <50ms (10k docs)
- Index size: ~100–200 KB per 1k documents
- Crawl time: depends on delay and depth (e.g., 10k docs @ delay=1s ≈ 3 hours)

## Troubleshooting

| Issue | Likely Cause | Fix |
|-------|--------------|-----|
| "Index not found" | No `data/index.json` | Run `npm run crawl` first |
| Empty results | Index doesn't contain query terms | Check domain whitelist, seeds, increase depth |
| Slow first query | Cold OS cache | Subsequent queries faster; consider warm-up |
| Tool not listed | Skill not in OpenClaw path or `skill.yaml` syntax error | `openclaw gateway restart`, check `skill.yaml` |
| Permission denied | OpenClaw user can't read skill dir | `chmod o+rx` skill directory or run OpenClaw as same user |

## Development

### Run Tests

```bash
# Install dev dependencies first
npm install

# Unit + integration (fast)
npm test

# Unit with coverage
npm run test:unit

# Integration only
npm run test:integration

# End-to-end (requires crawler implemented)
npm run test:e2e

# Benchmarks
npm run bench
```

### Project Structure

```
curated-search/
├── config.yaml           # Domain whitelist, crawl & search settings
├── skill.yaml            # OpenClaw skill manifest
├── package.json
├── README.md
├── SKILL.md              # Agent reference
├── PLAN.md               # Build plan (phases)
├── specs/                # Detailed technical specs
│   ├── PHASE-3-1-URL-DISCOVERY.md
│   ├── PHASE-3-2-CONTENT-EXTRACTION.md
│   ├── PHASE-3-3-POLITENESS-CONTROLS.md
│   ├── PHASE-3-4-ORCHESTRATION.md
│   ├── PHASE-4-SEARCH-TOOL-INTERFACE.md
│   ├── PHASE-5-OPENCLAW-INTEGRATION.md
│   ├── PHASE-6-TESTING-VALIDATION.md
│   ├── PHASE-7-DOCUMENTATION.md
│   └── PHASE-8-DEPLOYMENT-OPERATIONS.md
├── scripts/
│   └── search.js         # CLI tool (OpenClaw invokes this)
├── src/
│   ├── indexer.js        # MiniSearch wrapper, persistence
│   ├── crawler.js        # Domain-restricted crawler with politeness
│   ├── content-extractor.js
│   ├── url-normalizer.js
│   └── rate-limiter.js
├── test/
│   ├── unit/
│   ├── integration/
│   ├── e2e/
│   └── benchmarks/
├── data/
│   ├── index.json        # Search index (MiniSearch)
│   ├── index-docs.json   # Full documents (persisted)
│   └── crawl-state.json  # Crawler state for resume
└── logs/
    └── curated-search.log
```

## Adding New Domains

1. Edit `config.yaml`: add domain to `domains:` list (e.g., `- react.dev`)
2. Add corresponding seed URLs under `seeds:` (e.g., `"https://react.dev/learn"`)
3. Re-run crawl: `npm run crawl`

## License

MIT — see `LICENSE` file.

## Credits

- Built for OpenClaw
- Full-text search by [MiniSearch](https://github.com/lucaong/minisearch)
- Design: whitelist curation as the product


path: SECURITY_INCIDENT_2026-02-14.md
content: # Security Incident Report & Remediation

**Date:** 2026-02-14  
**Reporter:** VirusTotal (scan of ClawHub skill)  
**Skill:** curated-search v1.0.3  
**Classification:** Suspicious (attack surface: unused network server component)  

---

## Issue

The published skill bundle contained `src/search-api.js`, a fully functional HTTP/Unix socket server that is:
- **Not referenced** by the skill's `package.json` (main entry is `scripts/search.js`)
- **Not used** by the CLI tool or OpenClaw integration
- **Documented** in README as "legacy — not used"
- **Present** in the distributed package due to `.clawhubignore` not excluding `src/` files

If a user misconfigured their skill handler to use this server, it would expose a network listener with potential SSRF/other risks. While the code itself includes SSRF prevention, the *presence* of an unused server is a security liability and causes false-positive "suspicious" flags.

---

## Root Cause

- Legacy refactoring: `search-api.js` was replaced by CLI tool but not deleted
- `.clawhubignore` excluded only build artifacts, not source files or specific internal docs
- The file remained in the repository and was included in published tarballs

---

## Actions Taken

### 1. Immediate Containment
- ✅ Deleted `src/search-api.js` from repository
- ✅ Removed legacy reference in README.md
- ✅ Updated `.clawhubignore` to exclude:
  - `*AUDIT*.md`, `*CRITICAL*.md`, `*GAP*.md`
  - `SYS_*`, `YACY_*` (internal audit/research docs)
  - `securityaudit/`
- ✅ Prepared clean publish version 1.0.4

### 2. Registry Cleanup
- ✅ Deleted duplicate skill `curated-search-skill` (v1.0.0)
- ⚠️ Published v1.0.3 (first attempt succeeded but later registry checks showed "not found")
- ⏳ Rate limit prevented immediate republish; will retry with v1.0.4 after cooldown

### 3. Documentation
- ✅ Updated `CHANGELOG.md` with security fixes
- ✅ Created `SECURITY_INCIDENT_2026-02-14.md` (this file)
- ✅ Updated `IMPLEMENTATION_PLAN.md` to add security review step for future publishes

---

## Current State

| File | Status |
|------|--------|
| `src/search-api.js` | DELETED |
| `README.md` | Cleaned (no legacy reference) |
| `.clawhubignore` | Updated with comprehensive exclusions |
| `package.json` | Version bumped to 1.0.4 |
| `CHANGELOG.md` | Updated with security fixes |

**ClawHub status:**
- `curated-search-skill` → DELETED
- `curated-search` → *pending clean publish* (rate limited)

---

## Next Steps

1. **Wait for rate limit** (~1-2 hours from 08:49 PST)
2. **Run publish script** (see `scripts/publish-clean.sh`) or manually:
   ```bash
   clawhub publish /home/q/.openclaw/workspace/skills/curated-search \
     --slug curated-search \
     --name "Curated Search" \
     --version 1.0.4 \
     --changelog "Security: removed unused search-api.js server component and related references. Cleaned .clawhubignore to exclude all internal audit files." \
     --no-input
   ```
3. **Verify** published bundle does NOT contain `search-api.js` or any internal audit files
4. **Update MEMORY.md** with this incident and resolution

---

## Prevention

Going forward, before any `clawhub publish`:
- Run `git status` to check for unexpected files
- Review `.clawhubignore` coverage
- Inspect the tarball: `tar -tzf $(clawhub pack ...)` if needed
- Ensure only `scripts/`, `src/` (excluding legacy), `config.yaml` (template), `README.md`, `SKILL.md`, `CHANGELOG.md` are included

---

**Resolved by:** Lieutenant Qrusher  
**Approved by:** Captain JAQ


path: config.yaml
content: # Curated Search Configuration
# Edit this file to customize domains and crawl behavior
# See DOMAIN_GUIDE.md for rationale behind each domain

# =============================================================================
# DOMAIN WHITELIST
# =============================================================================
# These are the ONLY domains allowed in the search index.
# Crawler will reject any URL not matching these domains.

domains:
  # --- Programming Languages & Runtimes ---
  - python.org                    # Official Python docs
  - docs.python.org               # Python documentation subdomain
  - nodejs.org                    # Node.js runtime documentation
  - docs.nodejs.org               # Node docs subdomain (if exists)
  
  # --- Web Development ---
  - developer.mozilla.org         # MDN Web Docs (HTML, CSS, JS, Web APIs)
  
  # --- System Administration & Linux ---
  - man7.org                      # Linux man pages (system calls, C APIs)
  - linux.die.net                 # Alternative Linux man pages
  
  # --- Q&A and Community ---
  - stackoverflow.com             # Programming Q&A (high value, use carefully)
  - stackprinter.appspot.com      # Stack Overflow text-only (crawl-friendly)
  
  # --- Code Repositories ---
  - github.com                    # Code repos, READMEs
  - raw.githubusercontent.com     # Raw markdown files from GitHub
  
  # --- Encyclopedia & Reference ---
  - en.wikipedia.org              # Wikipedia (Computing/Technology topics)
  
  # --- Self-Documentation ---
  - docs.openclaw.ai              # OpenClaw own docs

# =============================================================================
# SEED URLS (Start Points for Crawling)
# =============================================================================
# These are the entry points where crawling begins.
# Using specific docs paths instead of homepages for better content.

seeds:
  # Python documentation
  - "https://docs.python.org/3/"
  - "https://docs.python.org/3/library/"
  - "https://docs.python.org/3/tutorial/"
  
  # Node.js documentation
  - "https://nodejs.org/api/"
  - "https://nodejs.org/docs/"
  
  # MDN Web Docs (focus on JavaScript and Web APIs)
  - "https://developer.mozilla.org/en-US/docs/Web/JavaScript"
  - "https://developer.mozilla.org/en-US/docs/Web/API"
  - "https://developer.mozilla.org/en-US/docs/Web/HTML"
  - "https://developer.mozilla.org/en-US/docs/Web/CSS"
  
  # Linux man pages
  - "https://man7.org/linux/man-pages/"
  - "https://linux.die.net/man/"
  
  # Stack Overflow (specific tags via text-friendly StackPrinter)
  # Note: These are examples. Full SO crawling requires different strategy.
  - "http://stackprinter.appspot.com/export?service=stackoverflow&language=en&width=640&height=800&tag=python&height=10"
  
  # GitHub (specific high-value repos - examples)
  - "https://github.com/nodejs/node/blob/main/README.md"
  - "https://github.com/python/cpython/blob/main/README.rst"
  
  # Wikipedia (Computing category entry point)
  - "https://en.wikipedia.org/wiki/Computer_programming"
  - "https://en.wikipedia.org/wiki/Linux"
  - "https://en.wikipedia.org/wiki/JavaScript"
  
  # OpenClaw docs
  - "https://docs.openclaw.ai/"

# =============================================================================
# CRAWL SETTINGS
# =============================================================================

crawl:
  # Depth: How many link hops from seeds to follow
  # 0 = index seeds only
  # 1 = seeds + direct links
  # 2 = recommended for MVP (covers most docs sites)
  # 3+ = deep crawl (use with caution)
  depth: 2
  
  # Delay: Milliseconds between requests to same host
  # 1000ms = 1 second = 60 requests/minute (polite)
  # 500ms = 120 requests/minute (aggressive, use on your own sites only)
  delay: 1000
  
  # Timeout: Seconds before giving up on a request
  timeout: 30
  
  # User agent: Identify yourself properly
  user_agent: "CuratedSearch/1.0 (OpenClaw Bot; +https://github.com/openclaw/openclaw)"
  
  # Max documents: Stop after indexing this many pages
  # Protects against runaway crawls
  max_documents: 10000
  
  # Concurrent requests: Parallel fetches per domain
  # 1 = sequential (safest for unknown sites)
  # 2-3 = moderate (use with known good sites)
  # 5+ = aggressive (risk of being blocked)
  concurrent_requests: 1
  
  # Respect robots.txt: Should we honor robots.txt disallow rules?
  respect_robots: true

# =============================================================================
# CONTENT FILTERING
# =============================================================================

# URL patterns to NEVER index (regular expressions)
# These are skipped regardless of domain
skip_patterns:
  - '^/search'                    # Internal search pages
  - '^/login'                     # Login forms
  - '^/logout'                    # Logout links
  - '^/admin'                     # Admin areas
  - '^/user/'                     # User profile pages (often low value)
  - '\.(pdf|zip|tar|gz|rar|exe|dmg|pkg|deb|rpm)$'  # Binary downloads
  - '\.(jpg|jpeg|png|gif|svg|ico|woff|woff2|ttf|eot)$'  # Images/fonts
  - '\.(css|js|map)$'             # Static assets
  - '\?(source|ref|utm)_'         # Tracking parameters
  - '#'                           # Fragment-only URLs
  - '/tag/'                       # Tag pages (often duplicate content)
  - '/category/'                  # Category listings (often duplicate)

# Content must be at least this many characters to be indexed
min_content_length: 200

# Maximum content size to index (characters)
# Prevents indexing huge pages (PDFs rendered as HTML, etc.)
max_content_length: 50000

# =============================================================================
# INDEX SETTINGS
# =============================================================================

index:
  # Path to index files (relative to skill directory)
  # Format: JSON files (MiniSearch serialized format)
  path: "data/index"
  
  # Auto-save: Save index after every N documents
  # 0 = only save at end of crawl
  # 10 = save frequently (slower but safer)
  # 100 = balanced
  auto_save_after: 100

# =============================================================================
# SEARCH SETTINGS
# =============================================================================

search:
  # Default result limit for queries
  default_limit: 10
  
  # Maximum results allowed per query
  max_limit: 100
  
  # Minimum score threshold for results
  # Results below this score are filtered out
  # 0.0 = no filtering
  # 0.3 = moderate quality filter
  min_score: 0.0

# =============================================================================
# LOGGING
# =============================================================================

logging:
  # Log level: debug, info, warn, error
  level: "info"
  
  # Log file path (relative to skill directory)
  file: "logs/curated-search.log"
  
  # Also print to console?
  console: true

# =============================================================================
# ENVIRONMENT OVERRIDES
# =============================================================================
# Uncomment to override for specific environments
# 
# development:
#   crawl:
#     depth: 1
#     delay: 100
#   logging:
#     level: "debug"
#
# production:
#   crawl:
#     depth: 2
#     delay: 2000
#     max_documents: 50000
#   logging:
#     level: "warn"
#     file: "/var/log/curated-search.log"


path: docs/deployment.md
content: # Deployment & Operations Guide

This guide covers running Curated Search in production-like environments: setup as a service, monitoring, backups, updates, and maintenance.

---

## 1. Installation Scenarios

### 1.1 Manual / Development

Already covered in `README.md`. Clone/copy skill, `npm install`, configure, `npm run crawl`.

### 1.2 System-wide (ClawHub)

Future: `openclaw skill install curated-search` will automate this.

---

## 2. Running the Crawler Periodically

The index needs to be refreshed periodically to capture updates. You can run the crawler:

- **Manually**: `npm run crawl`
- **Via cron**: Simple and effective
- **Via systemd timer**: Better logging, restart on failure

### 2.1 Cron Job (Simple)

```bash
# crontab -e (for user 'q')
0 2 * * 0 cd /home/q/.openclaw/workspace/skills/curated-search && /usr/bin/npm run crawl >> /var/log/curated-search/crawl.log 2>&1
```

Runs weekly at 2 AM Sunday. Adjust as needed.

### 2.2 systemd Timer (Recommended)

Provides structured logging, restart on failure, and status monitoring.

**Service file** (`systemd/curated-search-crawler.service`):

```ini
[Unit]
Description=Curated Search Crawler
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=q
WorkingDirectory=/home/q/.openclaw/workspace/skills/curated-search
ExecStart=/usr/bin/npm run crawl
# Standard output goes to journal
StandardOutput=journal
StandardError=journal
# Resource limits (optional)
MemoryLimit=500M
CPUQuota=50%
# On failure, don't retry immediately (timer controls next run)
```

**Timer file** (`systemd/curated-search-crawler.timer`):

```ini
[Unit]
Description=Weekly crawl of Curated Search index
Requires=curated-search-crawler.service

[Timer]
# Run weekly on Sunday at 2:00 AM
OnCalendar=weekly
Persistent=true
# Also run 5 minutes after boot if missed
OnBootSec=5min

[Install]
WantedBy=timers.target
```

**Install**:

```bash
# Copy to systemd directory (as root)
sudo cp systemd/curated-search-crawler.service systemd/curated-search-crawler.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now curated-search-crawler.timer
```

**Check status**:

```bash
systemctl status curated-search-crawler.timer
systemctl list-timers | grep curated
journalctl -u curated-search-crawler.service -f
```

---

## 3. OpenClaw Integration Persistence

OpenClaw loads the skill at startup. No separate daemon is needed for search queries.

**Ensure**:
- Skill directory is in OpenClaw's skill search path (workspace or global)
- `skill.yaml` is valid
- OpenClaw has been restarted after adding/changing skill

**Verification**:
```bash
openclaw tools list | grep curated-search
```

---

## 4. Logging

- **Crawler** (when run via systemd or cron): logs go to journal or file (redirection)
- **Search tool**: verbose logs go to stderr; OpenClaw captures these in its own logs
- **Application logs**: See `logs/curated-search.log` if configured (see `config.yaml`)

**Log rotation**: If writing to file, set up logrotate:

`/etc/logrotate.d/curated-search`:
```
/home/q/.openclaw/workspace/skills/curated-search/logs/*.log {
  daily
  rotate 7
  compress
  missingok
  notifempty
  create 640 q q
  sharedscripts
  postrotate
    systemctl reload openclaw > /dev/null 2>&1 || true
  endscript
}
```

---

## 5. Backup & Recovery

### 5.1 What to Back Up

- `config.yaml` — your domain whitelist and settings
- `data/index.json` and `data/index-docs.json` — the search index
- `data/crawl-state.json` — crawler resume state (optional)
- `logs/` — for historical analysis (optional)

Do **not** back up `node_modules/` (reinstall with `npm ci`).

### 5.2 Backup Command

```bash
tar -czf /backup/curated-search-$(date +%Y%m%d).tar.gz \
  --exclude=node_modules \
  --exclude=data/tmp \
  /home/q/.openclaw/workspace/skills/curated-search/
```

Store backups off-disk or remote.

### 5.3 Recovery Steps

1. Stop OpenClaw (optional)
2. Restore tarball: `tar -xzf backup.tar.gz -C /home/q/.openclaw/workspace/skills/`
3. Reinstall dependencies: `npm ci`
4. Restart OpenClaw if stopped
5. Test search: `openclaw tool curated-search.search --query="test"`

If index is corrupted:
```bash
rm data/index.json data/index-docs.json
npm run crawl
```

---

## 6. Health Monitoring

### 6.1 Manual Health Check

Run the search tool with a known query:

```bash
node scripts/search.js --query="test" --limit=1
```

Exit 0 and JSON output means healthy.

### 6.2 Automated Health Check Script

`scripts/health-check.js`:

```javascript
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const skillDir = path.resolve(__dirname, '..');

function fail(msg) {
  console.error('FAIL:', msg);
  process.exit(1);
}

// 1. Check index exists
const indexPath = path.join(skillDir, 'data', 'index.json');
if (!fs.existsSync(indexPath)) fail('Index missing');

// 2. Quick query test
try {
  const out = execSync('node scripts/search.js --query="test" --limit=1', {
    cwd: skillDir,
    encoding: 'utf8'
  });
  const res = JSON.parse(out);
  if (!Array.isArray(res)) fail('Invalid response format');
  console.log('OK: Index healthy');
} catch (e) {
  fail('Search failed: ' + e.message);
}
```

Make it executable: `chmod +x scripts/health-check.js`

**Cron check** (every hour):
```
0 * * * * /home/q/.openclaw/workspace/skills/curated-search/scripts/health-check.js || echo "ALERT: CuratedSearch down" | mail -s "CuratedSearch Alert" [REDACTED_SECRET]
```

---

## 7. Updates & Versioning

### 7.1 Skill Versioning

Follow semantic versioning in `skill.yaml`. Update version on breaking changes.

### 7.2 Updating From Git

```bash
cd /home/q/.openclaw/workspace/skills/curated-search
git pull origin main
npm ci                    # install new dependencies
# If config schema changed, merge config.yaml carefully
openclaw gateway restart
```

### 7.3 Rollback

```bash
git checkout v1.0.0
npm ci
openclaw gateway restart
```

Or restore from backup.

---

## 8. Performance Tuning

| Symptom | Likely Cause | Adjustment |
|---------|--------------|------------|
| Slow search | Cold index (first query) | Acceptable; subsequent fast |
| Slow search (persistent) | Index too large for memory | Reduce `max_documents` or upgrade RAM |
| Crawl takes days | Low `delay` + many domains | Increase `delay` to avoid blocking, or accept long crawl |
| No results | Whitelist too narrow, depth too low | Add domains to `domains`, increase `depth` |
| Blocked by sites | `delay` too aggressive | Increase `delay` (e.g., 2000ms) |

---

## 9. Security & Permissions

- Skill directory readable by OpenClaw user
- `config.yaml` may contain domain lists (no secrets)
- Crawler respects robots.txt; does not hammer servers
- No external network calls from search tool (pure local file read)

Recommended permissions:
```bash
chmod 750 ~/.openclaw/workspace/skills/curated-search
chmod 640 config.yaml
chmod 600 data/*.json  # optional
```

---

## 10. Disaster Scenarios

| Scenario | Recovery |
|----------|----------|
| Disk failure | Restore from backup (config + index) |
| Index corrupted | Delete `data/index*.json` and re-run `npm run crawl` |
| OpenClaw config lost | Re-link skill dir, restart OpenClaw |
| Node upgrade breaks | Use nvm to pin Node version; test upgrades in staging first |

---

## 11. Monitoring Metrics

Track these (manually or via script):

- **Index size** (`du -h data/index.json`)
- **Crawl duration** (log timestamp)
- **Documents indexed** (from stats in crawl log)
- **Search latency** (measure with `time node scripts/search.js --query=test`)
- **Disk free** (`df -h` on data partition)

---

## 12. Maintenance Schedule

- **Daily**: Nothing (unless crawler running continuously)
- **Weekly** (after timer crawl): Check cron/systemd logs for errors; verify disk space
- **Monthly**: Review domain whitelist (remove dead domains, add new ones); check index growth; test backup restore
- **Quarterly**: Full re-crawl (delete index first) to clear accumulated artifacts; update dependencies (`npm update` then test)

---

## 13. Additional Resources

- `README.md` — main user guide
- `SKILL.md` — agent reference
- `PLAN.md` — original build plan
- `specs/` — detailed technical specs (Phases 1–8)

---

**End of Deployment Guide**


path: package-lock.json
content: {
  "name": "curated-search",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "curated-search",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "jsdom": "^24.0.0",
        "marked": "^12.0.0",
        "minimist": "^1.2.8",
        "minisearch": "^6.3.0",
        "node-fetch": "^2.6.7",
        "robots-parser": "^2.2.1",
        "yaml": "^2.3.4"
      },
      "bin": {
        "curated-search": "scripts/search.js"
      },
      "devDependencies": {
        "jest": "^29.7.0",
        "nock": "^13.4.0",
        "tmp": "^0.2.1"
      }
    },
    "node_modules/@asamuzakjp/css-color": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@csstools/css-calc": "^2.1.3",
        "@csstools/css-color-parser": "^3.0.9",
        "@csstools/css-parser-algorithms": "^3.0.4",
        "@csstools/css-tokenizer": "^3.0.3",
        "lru-cache": "^10.4.3"
      }
    },
    "node_modules/@babel/code-frame": {
      "version": "7.29.0",
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-validator-identifier": "^7.28.5",
        "js-tokens": "^4.0.0",
        "picocolors": "^1.1.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/compat-data": {
      "version": "7.29.0",
      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/core": {
      "version": "7.29.0",
      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.29.0",
        "@babel/generator": "^7.29.0",
        "@babel/helper-compilation-targets": "^7.28.6",
        "@babel/helper-module-transforms": "^7.28.6",
        "@babel/helpers": "^7.28.6",
        "@babel/parser": "^7.29.0",
        "@babel/template": "^7.28.6",
        "@babel/traverse": "^7.29.0",
        "@babel/types": "^7.29.0",
        "@jridgewell/remapping": "^2.3.5",
        "convert-source-map": "^2.0.0",
        "debug": "^4.1.0",
        "gensync": "^1.0.0-beta.2",
        "json5": "^2.2.3",
        "semver": "^6.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/babel"
      }
    },
    "node_modules/@babel/generator": {
      "version": "7.29.1",
      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/parser": "^7.29.0",
        "@babel/types": "^7.29.0",
        "@jridgewell/gen-mapping": "^0.3.12",
        "@jridgewell/trace-mapping": "^0.3.28",
        "jsesc": "^3.0.2"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-compilation-targets": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/compat-data": "^7.28.6",
        "@babel/helper-validator-option": "^7.27.1",
        "browserslist": "^4.24.0",
        "lru-cache": "^5.1.1",
        "semver": "^6.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
      "version": "5.1.1",
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "yallist": "^3.0.2"
      }
    },
    "node_modules/@babel/helper-globals": {
      "version": "7.28.0",
      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-module-imports": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/traverse": "^7.28.6",
        "@babel/types": "^7.28.6"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-module-transforms": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-module-imports": "^7.28.6",
        "@babel/helper-validator-identifier": "^7.28.5",
        "@babel/traverse": "^7.28.6"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/helper-plugin-utils": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-string-parser": {
      "version": "7.27.1",
      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-validator-identifier": {
      "version": "7.28.5",
      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-validator-option": {
      "version": "7.27.1",
      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helpers": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/template": "^7.28.6",
        "@babel/types": "^7.28.6"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/parser": {
      "version": "7.29.0",
      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.29.0"
      },
      "bin": {
        "parser": "bin/babel-parser.js"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@babel/plugin-syntax-async-generators": {
      "version": "7.8.4",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-bigint": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-class-properties": {
      "version": "7.12.13",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.12.13"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-class-static-block": {
      "version": "7.14.5",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.14.5"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-import-attributes": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.28.6"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-import-meta": {
      "version": "7.10.4",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.10.4"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-json-strings": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-jsx": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.28.6"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
      "version": "7.10.4",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.10.4"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-numeric-separator": {
      "version": "7.10.4",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.10.4"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-object-rest-spread": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-optional-catch-binding": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-optional-chaining": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-private-property-in-object": {
      "version": "7.14.5",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.14.5"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-top-level-await": {
      "version": "7.14.5",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.14.5"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-typescript": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.28.6"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/template": {
      "version": "7.28.6",
      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.28.6",
        "@babel/parser": "^7.28.6",
        "@babel/types": "^7.28.6"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/traverse": {
      "version": "7.29.0",
      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.29.0",
        "@babel/generator": "^7.29.0",
        "@babel/helper-globals": "^7.28.0",
        "@babel/parser": "^7.29.0",
        "@babel/template": "^7.28.6",
        "@babel/types": "^7.29.0",
        "debug": "^4.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/types": {
      "version": "7.29.0",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.27.1",
        "@babel/helper-validator-identifier": "^7.28.5"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@bcoe/v8-coverage": {
      "version": "0.2.3",
      "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@csstools/color-helpers": {
      "version": "5.1.0",
      "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/csstools"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/csstools"
        }
      ],
      "license": "MIT-0",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@csstools/css-calc": {
      "version": "2.1.4",
      "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/csstools"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/csstools"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@csstools/css-parser-algorithms": "^3.0.5",
        "@csstools/css-tokenizer": "^3.0.4"
      }
    },
    "node_modules/@csstools/css-color-parser": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/csstools"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/csstools"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "@csstools/color-helpers": "^5.1.0",
        "@csstools/css-calc": "^2.1.4"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@csstools/css-parser-algorithms": "^3.0.5",
        "@csstools/css-tokenizer": "^3.0.4"
      }
    },
    "node_modules/@csstools/css-parser-algorithms": {
      "version": "3.0.5",
      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/csstools"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/csstools"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@csstools/css-tokenizer": "^3.0.4"
      }
    },
    "node_modules/@csstools/css-tokenizer": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/csstools"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/csstools"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@istanbuljs/load-nyc-config": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "camelcase": "^5.3.1",
        "find-up": "^4.1.0",
        "get-package-type": "^0.1.0",
        "js-yaml": "^3.13.1",
        "resolve-from": "^5.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@istanbuljs/schema": {
      "version": "0.1.3",
      "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/console": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "jest-message-util": "^29.7.0",
        "jest-util": "^29.7.0",
        "slash": "^3.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/core": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/console": "^29.7.0",
        "@jest/reporters": "^29.7.0",
        "@jest/test-result": "^29.7.0",
        "@jest/transform": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "ansi-escapes": "^4.2.1",
        "chalk": "^4.0.0",
        "ci-info": "^3.2.0",
        "exit": "^0.1.2",
        "graceful-fs": "^4.2.9",
        "jest-changed-files": "^29.7.0",
        "jest-config": "^29.7.0",
        "jest-haste-map": "^29.7.0",
        "jest-message-util": "^29.7.0",
        "jest-regex-util": "^29.6.3",
        "jest-resolve": "^29.7.0",
        "jest-resolve-dependencies": "^29.7.0",
        "jest-runner": "^29.7.0",
        "jest-runtime": "^29.7.0",
        "jest-snapshot": "^29.7.0",
        "jest-util": "^29.7.0",
        "jest-validate": "^29.7.0",
        "jest-watcher": "^29.7.0",
        "micromatch": "^4.0.4",
        "pretty-format": "^29.7.0",
        "slash": "^3.0.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      },
      "peerDependencies": {
        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
      },
      "peerDependenciesMeta": {
        "node-notifier": {
          "optional": true
        }
      }
    },
    "node_modules/@jest/environment": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/fake-timers": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "jest-mock": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/expect": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "expect": "^29.7.0",
        "jest-snapshot": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/expect-utils": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "jest-get-type": "^29.6.3"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/fake-timers": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/types": "^29.6.3",
        "@sinonjs/fake-timers": "^10.0.2",
        "@types/node": "*",
        "jest-message-util": "^29.7.0",
        "jest-mock": "^29.7.0",
        "jest-util": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/globals": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/environment": "^29.7.0",
        "@jest/expect": "^29.7.0",
        "@jest/types": "^29.6.3",
        "jest-mock": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/reporters": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@bcoe/v8-coverage": "^0.2.3",
        "@jest/console": "^29.7.0",
        "@jest/test-result": "^29.7.0",
        "@jest/transform": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@jridgewell/trace-mapping": "^0.3.18",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "collect-v8-coverage": "^1.0.0",
        "exit": "^0.1.2",
        "glob": "^7.1.3",
        "graceful-fs": "^4.2.9",
        "istanbul-lib-coverage": "^3.0.0",
        "istanbul-lib-instrument": "^6.0.0",
        "istanbul-lib-report": "^3.0.0",
        "istanbul-lib-source-maps": "^4.0.0",
        "istanbul-reports": "^3.1.3",
        "jest-message-util": "^29.7.0",
        "jest-util": "^29.7.0",
        "jest-worker": "^29.7.0",
        "slash": "^3.0.0",
        "string-length": "^4.0.1",
        "strip-ansi": "^6.0.0",
        "v8-to-istanbul": "^9.0.1"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      },
      "peerDependencies": {
        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
      },
      "peerDependenciesMeta": {
        "node-notifier": {
          "optional": true
        }
      }
    },
    "node_modules/@jest/schemas": {
      "version": "29.6.3",
      "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@sinclair/typebox": "^0.27.8"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/source-map": {
      "version": "29.6.3",
      "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/trace-mapping": "^0.3.18",
        "callsites": "^3.0.0",
        "graceful-fs": "^4.2.9"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/test-result": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/console": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@types/istanbul-lib-coverage": "^2.0.0",
        "collect-v8-coverage": "^1.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/test-sequencer": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/test-result": "^29.7.0",
        "graceful-fs": "^4.2.9",
        "jest-haste-map": "^29.7.0",
        "slash": "^3.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/transform": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/core": "^7.11.6",
        "@jest/types": "^29.6.3",
        "@jridgewell/trace-mapping": "^0.3.18",
        "babel-plugin-istanbul": "^6.1.1",
        "chalk": "^4.0.0",
        "convert-source-map": "^2.0.0",
        "fast-json-stable-stringify": "^2.1.0",
        "graceful-fs": "^4.2.9",
        "jest-haste-map": "^29.7.0",
        "jest-regex-util": "^29.6.3",
        "jest-util": "^29.7.0",
        "micromatch": "^4.0.4",
        "pirates": "^4.0.4",
        "slash": "^3.0.0",
        "write-file-atomic": "^4.0.2"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jest/types": {
      "version": "29.6.3",
      "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/schemas": "^29.6.3",
        "@types/istanbul-lib-coverage": "^2.0.0",
        "@types/istanbul-reports": "^3.0.0",
        "@types/node": "*",
        "@types/yargs": "^17.0.8",
        "chalk": "^4.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/@jridgewell/gen-mapping": {
      "version": "0.3.13",
      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.0",
        "@jridgewell/trace-mapping": "^0.3.24"
      }
    },
    "node_modules/@jridgewell/remapping": {
      "version": "2.3.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/gen-mapping": "^0.3.5",
        "@jridgewell/trace-mapping": "^0.3.24"
      }
    },
    "node_modules/@jridgewell/resolve-uri": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@jridgewell/sourcemap-codec": {
      "version": "1.5.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@jridgewell/trace-mapping": {
      "version": "0.3.31",
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/resolve-uri": "^3.1.0",
        "@jridgewell/sourcemap-codec": "^1.4.14"
      }
    },
    "node_modules/@sinclair/typebox": {
      "version": "0.27.10",
      "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@sinonjs/commons": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "type-detect": "4.0.8"
      }
    },
    "node_modules/@sinonjs/fake-timers": {
      "version": "10.3.0",
      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "@sinonjs/commons": "^3.0.0"
      }
    },
    "node_modules/@types/babel__core": {
      "version": "7.20.5",
      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/parser": "^7.20.7",
        "@babel/types": "^7.20.7",
        "@types/babel__generator": "*",
        "@types/babel__template": "*",
        "@types/babel__traverse": "*"
      }
    },
    "node_modules/@types/babel__generator": {
      "version": "7.27.0",
      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.0.0"
      }
    },
    "node_modules/@types/babel__template": {
      "version": "7.4.4",
      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/parser": "^7.1.0",
        "@babel/types": "^7.0.0"
      }
    },
    "node_modules/@types/babel__traverse": {
      "version": "7.28.0",
      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.28.2"
      }
    },
    "node_modules/@types/graceful-fs": {
      "version": "4.1.9",
      "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/@types/istanbul-lib-coverage": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/istanbul-lib-report": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/istanbul-lib-coverage": "*"
      }
    },
    "node_modules/@types/istanbul-reports": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/istanbul-lib-report": "*"
      }
    },
    "node_modules/@types/node": {
      "version": "25.2.3",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~7.16.0"
      }
    },
    "node_modules/@types/stack-utils": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/yargs": {
      "version": "17.0.35",
      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/yargs-parser": "*"
      }
    },
    "node_modules/@types/yargs-parser": {
      "version": "21.0.3",
      "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/agent-base": {
      "version": "7.1.4",
      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/ansi-escapes": {
      "version": "4.3.2",
      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "type-fest": "^0.21.3"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/anymatch": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "normalize-path": "^3.0.0",
        "picomatch": "^2.0.4"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/argparse": {
      "version": "1.0.10",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "sprintf-js": "~1.0.2"
      }
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/babel-jest": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/transform": "^29.7.0",
        "@types/babel__core": "^7.1.14",
        "babel-plugin-istanbul": "^6.1.1",
        "babel-preset-jest": "^29.6.3",
        "chalk": "^4.0.0",
        "graceful-fs": "^4.2.9",
        "slash": "^3.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.8.0"
      }
    },
    "node_modules/babel-plugin-istanbul": {
      "version": "6.1.1",
      "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.0.0",
        "@istanbuljs/load-nyc-config": "^1.0.0",
        "@istanbuljs/schema": "^0.1.2",
        "istanbul-lib-instrument": "^5.0.4",
        "test-exclude": "^6.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "@babel/core": "^7.12.3",
        "@babel/parser": "^7.14.7",
        "@istanbuljs/schema": "^0.1.2",
        "istanbul-lib-coverage": "^3.2.0",
        "semver": "^6.3.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/babel-plugin-jest-hoist": {
      "version": "29.6.3",
      "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/template": "^7.3.3",
        "@babel/types": "^7.3.3",
        "@types/babel__core": "^7.1.14",
        "@types/babel__traverse": "^7.0.6"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/babel-preset-current-node-syntax": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/plugin-syntax-async-generators": "^7.8.4",
        "@babel/plugin-syntax-bigint": "^7.8.3",
        "@babel/plugin-syntax-class-properties": "^7.12.13",
        "@babel/plugin-syntax-class-static-block": "^7.14.5",
        "@babel/plugin-syntax-import-attributes": "^7.24.7",
        "@babel/plugin-syntax-import-meta": "^7.10.4",
        "@babel/plugin-syntax-json-strings": "^7.8.3",
        "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
        "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
        "@babel/plugin-syntax-numeric-separator": "^7.10.4",
        "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
        "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
        "@babel/plugin-syntax-optional-chaining": "^7.8.3",
        "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
        "@babel/plugin-syntax-top-level-await": "^7.14.5"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0 || ^8.0.0-0"
      }
    },
    "node_modules/babel-preset-jest": {
      "version": "29.6.3",
      "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "babel-plugin-jest-hoist": "^29.6.3",
        "babel-preset-current-node-syntax": "^1.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/baseline-browser-mapping": {
      "version": "2.9.19",
      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "baseline-browser-mapping": "dist/cli.js"
      }
    },
    "node_modules/brace-expansion": {
      "version": "1.1.12",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "balanced-match": "^1.0.0",
        "concat-map": "0.0.1"
      }
    },
    "node_modules/braces": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "fill-range": "^7.1.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/browserslist": {
      "version": "4.28.1",
      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/browserslist"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/browserslist"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "baseline-browser-mapping": "^2.9.0",
        "caniuse-lite": "^1.0.30001759",
        "electron-to-chromium": "^1.5.263",
        "node-releases": "^2.0.27",
        "update-browserslist-db": "^1.2.0"
      },
      "bin": {
        "browserslist": "cli.js"
      },
      "engines": {
        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
      }
    },
    "node_modules/bser": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "node-int64": "^0.4.0"
      }
    },
    "node_modules/buffer-from": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/callsites": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/camelcase": {
      "version": "5.3.1",
      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/caniuse-lite": {
      "version": "1.0.30001769",
      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/browserslist"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "CC-BY-4.0"
    },
    "node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/char-regex": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/ci-info": {
      "version": "3.9.0",
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/sibiraj-s"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/cjs-module-lexer": {
      "version": "1.4.3",
      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/cliui": {
      "version": "8.0.1",
      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "string-width": "^4.2.0",
        "strip-ansi": "^6.0.1",
        "wrap-ansi": "^7.0.0"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/co": {
      "version": "4.6.0",
      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "iojs": ">= 1.0.0",
        "node": ">= 0.12.0"
      }
    },
    "node_modules/collect-v8-coverage": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "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": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/convert-source-map": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/create-jest": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/types": "^29.6.3",
        "chalk": "^4.0.0",
        "exit": "^0.1.2",
        "graceful-fs": "^4.2.9",
        "jest-config": "^29.7.0",
        "jest-util": "^29.7.0",
        "prompts": "^2.0.1"
      },
      "bin": {
        "create-jest": "bin/create-jest.js"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/cssstyle": {
      "version": "4.6.0",
      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@asamuzakjp/css-color": "^3.2.0",
        "rrweb-cssom": "^0.8.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/cssstyle/node_modules/rrweb-cssom": {
      "version": "0.8.0",
      "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/data-urls": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "whatwg-mimetype": "^4.0.0",
        "whatwg-url": "^14.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/decimal.js": {
      "version": "10.6.0",
      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/dedent": {
      "version": "1.7.1",
      "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "peerDependencies": {
        "babel-plugin-macros": "^3.1.0"
      },
      "peerDependenciesMeta": {
        "babel-plugin-macros": {
          "optional": true
        }
      }
    },
    "node_modules/deepmerge": {
      "version": "4.3.1",
      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/detect-newline": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/diff-sequences": {
      "version": "29.6.3",
      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/electron-to-chromium": {
      "version": "1.5.286",
      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/emittery": {
      "version": "0.13.1",
      "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sindresorhus/emittery?sponsor=1"
      }
    },
    "node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/entities": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.12"
      },
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/error-ex": {
      "version": "1.3.4",
      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "is-arrayish": "^0.2.1"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escalade": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/escape-string-regexp": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/esprima": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-2-Clause",
      "bin": {
        "esparse": "bin/esparse.js",
        "esvalidate": "bin/esvalidate.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/execa": {
      "version": "5.1.1",
      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "cross-spawn": "^7.0.3",
        "get-stream": "^6.0.0",
        "human-signals": "^2.1.0",
        "is-stream": "^2.0.0",
        "merge-stream": "^2.0.0",
        "npm-run-path": "^4.0.1",
        "onetime": "^5.1.2",
        "signal-exit": "^3.0.3",
        "strip-final-newline": "^2.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sindresorhus/execa?sponsor=1"
      }
    },
    "node_modules/exit": {
      "version": "0.1.2",
      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/expect": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/expect-utils": "^29.7.0",
        "jest-get-type": "^29.6.3",
        "jest-matcher-utils": "^29.7.0",
        "jest-message-util": "^29.7.0",
        "jest-util": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/fast-json-stable-stringify": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/fb-watchman": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "bser": "2.1.1"
      }
    },
    "node_modules/fill-range": {
      "version": "7.1.1",
      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "to-regex-range": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/find-up": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "locate-path": "^5.0.0",
        "path-exists": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/fs.realpath": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/gensync": {
      "version": "1.0.0-beta.2",
      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/get-caller-file": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": "6.* || 8.* || >= 10.*"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-package-type": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8.0.0"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/get-stream": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/glob": {
      "version": "7.2.3",
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [REDACTED_SECRET]",
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "fs.realpath": "^1.0.0",
        "inflight": "^1.0.4",
        "inherits": "2",
        "minimatch": "^3.1.1",
        "once": "^1.3.0",
        "path-is-absolute": "^1.0.0"
      },
      "engines": {
        "node": "*"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/graceful-fs": {
      "version": "4.2.11",
      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/html-encoding-sniffer": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "whatwg-encoding": "^3.1.1"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/html-escaper": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/http-proxy-agent": {
      "version": "7.0.2",
      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "agent-base": "^7.1.0",
        "debug": "^4.3.4"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/https-proxy-agent": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "agent-base": "^7.1.2",
        "debug": "4"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/human-signals": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": ">=10.17.0"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/import-local": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "pkg-dir": "^4.2.0",
        "resolve-cwd": "^3.0.0"
      },
      "bin": {
        "import-local-fixture": "fixtures/cli.js"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/imurmurhash": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.8.19"
      }
    },
    "node_modules/inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "once": "^1.3.0",
        "wrappy": "1"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/is-arrayish": {
      "version": "0.2.1",
      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/is-core-module": {
      "version": "2.16.1",
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "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": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/is-generator-fn": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/is-number": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.12.0"
      }
    },
    "node_modules/is-potential-custom-element-name": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/is-stream": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/istanbul-lib-coverage": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/istanbul-lib-instrument": {
      "version": "6.0.3",
      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "@babel/core": "^7.23.9",
        "@babel/parser": "^7.23.9",
        "@istanbuljs/schema": "^0.1.3",
        "istanbul-lib-coverage": "^3.2.0",
        "semver": "^7.5.4"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/istanbul-lib-instrument/node_modules/semver": {
      "version": "7.7.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/istanbul-lib-report": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "istanbul-lib-coverage": "^3.0.0",
        "make-dir": "^4.0.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/istanbul-lib-source-maps": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "debug": "^4.1.1",
        "istanbul-lib-coverage": "^3.0.0",
        "source-map": "^0.6.1"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/istanbul-reports": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "html-escaper": "^2.0.0",
        "istanbul-lib-report": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/core": "^29.7.0",
        "@jest/types": "^29.6.3",
        "import-local": "^3.0.2",
        "jest-cli": "^29.7.0"
      },
      "bin": {
        "jest": "bin/jest.js"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      },
      "peerDependencies": {
        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
      },
      "peerDependenciesMeta": {
        "node-notifier": {
          "optional": true
        }
      }
    },
    "node_modules/jest-changed-files": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "execa": "^5.0.0",
        "jest-util": "^29.7.0",
        "p-limit": "^3.1.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-circus": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/environment": "^29.7.0",
        "@jest/expect": "^29.7.0",
        "@jest/test-result": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "co": "^4.6.0",
        "dedent": "^1.0.0",
        "is-generator-fn": "^2.0.0",
        "jest-each": "^29.7.0",
        "jest-matcher-utils": "^29.7.0",
        "jest-message-util": "^29.7.0",
        "jest-runtime": "^29.7.0",
        "jest-snapshot": "^29.7.0",
        "jest-util": "^29.7.0",
        "p-limit": "^3.1.0",
        "pretty-format": "^29.7.0",
        "pure-rand": "^6.0.0",
        "slash": "^3.0.0",
        "stack-utils": "^2.0.3"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-cli": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/core": "^29.7.0",
        "@jest/test-result": "^29.7.0",
        "@jest/types": "^29.6.3",
        "chalk": "^4.0.0",
        "create-jest": "^29.7.0",
        "exit": "^0.1.2",
        "import-local": "^3.0.2",
        "jest-config": "^29.7.0",
        "jest-util": "^29.7.0",
        "jest-validate": "^29.7.0",
        "yargs": "^17.3.1"
      },
      "bin": {
        "jest": "bin/jest.js"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      },
      "peerDependencies": {
        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
      },
      "peerDependenciesMeta": {
        "node-notifier": {
          "optional": true
        }
      }
    },
    "node_modules/jest-config": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/core": "^7.11.6",
        "@jest/test-sequencer": "^29.7.0",
        "@jest/types": "^29.6.3",
        "babel-jest": "^29.7.0",
        "chalk": "^4.0.0",
        "ci-info": "^3.2.0",
        "deepmerge": "^4.2.2",
        "glob": "^7.1.3",
        "graceful-fs": "^4.2.9",
        "jest-circus": "^29.7.0",
        "jest-environment-node": "^29.7.0",
        "jest-get-type": "^29.6.3",
        "jest-regex-util": "^29.6.3",
        "jest-resolve": "^29.7.0",
        "jest-runner": "^29.7.0",
        "jest-util": "^29.7.0",
        "jest-validate": "^29.7.0",
        "micromatch": "^4.0.4",
        "parse-json": "^5.2.0",
        "pretty-format": "^29.7.0",
        "slash": "^3.0.0",
        "strip-json-comments": "^3.1.1"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      },
      "peerDependencies": {
        "@types/node": "*",
        "ts-node": ">=9.0.0"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        },
        "ts-node": {
          "optional": true
        }
      }
    },
    "node_modules/jest-diff": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "chalk": "^4.0.0",
        "diff-sequences": "^29.6.3",
        "jest-get-type": "^29.6.3",
        "pretty-format": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-docblock": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "detect-newline": "^3.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-each": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/types": "^29.6.3",
        "chalk": "^4.0.0",
        "jest-get-type": "^29.6.3",
        "jest-util": "^29.7.0",
        "pretty-format": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-environment-node": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/environment": "^29.7.0",
        "@jest/fake-timers": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "jest-mock": "^29.7.0",
        "jest-util": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-get-type": {
      "version": "29.6.3",
      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-haste-map": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/types": "^29.6.3",
        "@types/graceful-fs": "^4.1.3",
        "@types/node": "*",
        "anymatch": "^3.0.3",
        "fb-watchman": "^2.0.0",
        "graceful-fs": "^4.2.9",
        "jest-regex-util": "^29.6.3",
        "jest-util": "^29.7.0",
        "jest-worker": "^29.7.0",
        "micromatch": "^4.0.4",
        "walker": "^1.0.8"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      },
      "optionalDependencies": {
        "fsevents": "^2.3.2"
      }
    },
    "node_modules/jest-leak-detector": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "jest-get-type": "^29.6.3",
        "pretty-format": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-matcher-utils": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "chalk": "^4.0.0",
        "jest-diff": "^29.7.0",
        "jest-get-type": "^29.6.3",
        "pretty-format": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-message-util": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.12.13",
        "@jest/types": "^29.6.3",
        "@types/stack-utils": "^2.0.0",
        "chalk": "^4.0.0",
        "graceful-fs": "^4.2.9",
        "micromatch": "^4.0.4",
        "pretty-format": "^29.7.0",
        "slash": "^3.0.0",
        "stack-utils": "^2.0.3"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-mock": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "jest-util": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-pnp-resolver": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      },
      "peerDependencies": {
        "jest-resolve": "*"
      },
      "peerDependenciesMeta": {
        "jest-resolve": {
          "optional": true
        }
      }
    },
    "node_modules/jest-regex-util": {
      "version": "29.6.3",
      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-resolve": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "chalk": "^4.0.0",
        "graceful-fs": "^4.2.9",
        "jest-haste-map": "^29.7.0",
        "jest-pnp-resolver": "^1.2.2",
        "jest-util": "^29.7.0",
        "jest-validate": "^29.7.0",
        "resolve": "^1.20.0",
        "resolve.exports": "^2.0.0",
        "slash": "^3.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-resolve-dependencies": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "jest-regex-util": "^29.6.3",
        "jest-snapshot": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-runner": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/console": "^29.7.0",
        "@jest/environment": "^29.7.0",
        "@jest/test-result": "^29.7.0",
        "@jest/transform": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "emittery": "^0.13.1",
        "graceful-fs": "^4.2.9",
        "jest-docblock": "^29.7.0",
        "jest-environment-node": "^29.7.0",
        "jest-haste-map": "^29.7.0",
        "jest-leak-detector": "^29.7.0",
        "jest-message-util": "^29.7.0",
        "jest-resolve": "^29.7.0",
        "jest-runtime": "^29.7.0",
        "jest-util": "^29.7.0",
        "jest-watcher": "^29.7.0",
        "jest-worker": "^29.7.0",
        "p-limit": "^3.1.0",
        "source-map-support": "0.5.13"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-runtime": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/environment": "^29.7.0",
        "@jest/fake-timers": "^29.7.0",
        "@jest/globals": "^29.7.0",
        "@jest/source-map": "^29.6.3",
        "@jest/test-result": "^29.7.0",
        "@jest/transform": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "cjs-module-lexer": "^1.0.0",
        "collect-v8-coverage": "^1.0.0",
        "glob": "^7.1.3",
        "graceful-fs": "^4.2.9",
        "jest-haste-map": "^29.7.0",
        "jest-message-util": "^29.7.0",
        "jest-mock": "^29.7.0",
        "jest-regex-util": "^29.6.3",
        "jest-resolve": "^29.7.0",
        "jest-snapshot": "^29.7.0",
        "jest-util": "^29.7.0",
        "slash": "^3.0.0",
        "strip-bom": "^4.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-snapshot": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/core": "^7.11.6",
        "@babel/generator": "^7.7.2",
        "@babel/plugin-syntax-jsx": "^7.7.2",
        "@babel/plugin-syntax-typescript": "^7.7.2",
        "@babel/types": "^7.3.3",
        "@jest/expect-utils": "^29.7.0",
        "@jest/transform": "^29.7.0",
        "@jest/types": "^29.6.3",
        "babel-preset-current-node-syntax": "^1.0.0",
        "chalk": "^4.0.0",
        "expect": "^29.7.0",
        "graceful-fs": "^4.2.9",
        "jest-diff": "^29.7.0",
        "jest-get-type": "^29.6.3",
        "jest-matcher-utils": "^29.7.0",
        "jest-message-util": "^29.7.0",
        "jest-util": "^29.7.0",
        "natural-compare": "^1.4.0",
        "pretty-format": "^29.7.0",
        "semver": "^7.5.3"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-snapshot/node_modules/semver": {
      "version": "7.7.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/jest-util": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "ci-info": "^3.2.0",
        "graceful-fs": "^4.2.9",
        "picomatch": "^2.2.3"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-validate": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/types": "^29.6.3",
        "camelcase": "^6.2.0",
        "chalk": "^4.0.0",
        "jest-get-type": "^29.6.3",
        "leven": "^3.1.0",
        "pretty-format": "^29.7.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-validate/node_modules/camelcase": {
      "version": "6.3.0",
      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/jest-watcher": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/test-result": "^29.7.0",
        "@jest/types": "^29.6.3",
        "@types/node": "*",
        "ansi-escapes": "^4.2.1",
        "chalk": "^4.0.0",
        "emittery": "^0.13.1",
        "jest-util": "^29.7.0",
        "string-length": "^4.0.1"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-worker": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/node": "*",
        "jest-util": "^29.7.0",
        "merge-stream": "^2.0.0",
        "supports-color": "^8.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/jest-worker/node_modules/supports-color": {
      "version": "8.1.1",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/supports-color?sponsor=1"
      }
    },
    "node_modules/js-tokens": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/js-yaml": {
      "version": "3.14.2",
      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "argparse": "^1.0.7",
        "esprima": "^4.0.0"
      },
      "bin": {
        "js-yaml": "bin/js-yaml.js"
      }
    },
    "node_modules/jsdom": {
      "version": "24.1.3",
      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "cssstyle": "^4.0.1",
        "data-urls": "^5.0.0",
        "decimal.js": "^10.4.3",
        "form-data": "^4.0.0",
        "html-encoding-sniffer": "^4.0.0",
        "http-proxy-agent": "^7.0.2",
        "https-proxy-agent": "^7.0.5",
        "is-potential-custom-element-name": "^1.0.1",
        "nwsapi": "^2.2.12",
        "parse5": "^7.1.2",
        "rrweb-cssom": "^0.7.1",
        "saxes": "^6.0.0",
        "symbol-tree": "^3.2.4",
        "tough-cookie": "^4.1.4",
        "w3c-xmlserializer": "^5.0.0",
        "webidl-conversions": "^7.0.0",
        "whatwg-encoding": "^3.1.1",
        "whatwg-mimetype": "^4.0.0",
        "whatwg-url": "^14.0.0",
        "ws": "^8.18.0",
        "xml-name-validator": "^5.0.0"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "canvas": "^2.11.2"
      },
      "peerDependenciesMeta": {
        "canvas": {
          "optional": true
        }
      }
    },
    "node_modules/jsesc": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "bin": {
        "jsesc": "bin/jsesc"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/json-parse-even-better-errors": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/json-stringify-safe": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/json5": {
      "version": "2.2.3",
      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "bin": {
        "json5": "lib/cli.js"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/kleur": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/leven": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/lines-and-columns": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/locate-path": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "p-locate": "^4.1.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/lru-cache": {
      "version": "10.4.3",
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/make-dir": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "semver": "^7.5.3"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/make-dir/node_modules/semver": {
      "version": "7.7.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/makeerror": {
      "version": "1.0.12",
      "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "tmpl": "1.0.5"
      }
    },
    "node_modules/marked": {
      "version": "12.0.2",
      "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "marked": "bin/marked.js"
      },
      "engines": {
        "node": ">= 18"
      }
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/merge-stream": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/micromatch": {
      "version": "4.0.8",
      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "braces": "^3.0.3",
        "picomatch": "^2.3.1"
      },
      "engines": {
        "node": ">=8.6"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mimic-fn": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/minimatch": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "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": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/minisearch": {
      "version": "6.3.0",
      "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/natural-compare": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/nock": {
      "version": "13.5.6",
      "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "debug": "^4.1.0",
        "json-stringify-safe": "^5.0.1",
        "propagate": "^2.0.0"
      },
      "engines": {
        "node": ">= 10.13"
      }
    },
    "node_modules/node-fetch": {
      "version": "2.7.0",
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "whatwg-url": "^5.0.0"
      },
      "engines": {
        "node": "4.x || >=6.0.0"
      },
      "peerDependencies": {
        "encoding": "^0.1.0"
      },
      "peerDependenciesMeta": {
        "encoding": {
          "optional": true
        }
      }
    },
    "node_modules/node-fetch/node_modules/tr46": {
      "version": "0.0.3",
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/node-fetch/node_modules/webidl-conversions": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause"
    },
    "node_modules/node-fetch/node_modules/whatwg-url": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "tr46": "~0.0.3",
        "webidl-conversions": "^3.0.0"
      }
    },
    "node_modules/node-int64": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/node-releases": {
      "version": "2.0.27",
      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/normalize-path": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/npm-run-path": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/nwsapi": {
      "version": "2.2.23",
      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/onetime": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "mimic-fn": "^2.1.0"
      },
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-limit": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "yocto-queue": "^0.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-locate": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "p-limit": "^2.2.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/p-locate/node_modules/p-limit": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "p-try": "^2.0.0"
      },
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-try": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/parse-json": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.0.0",
        "error-ex": "^1.3.1",
        "json-parse-even-better-errors": "^2.3.0",
        "lines-and-columns": "^1.1.6"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/parse5": {
      "version": "7.3.0",
      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "entities": "^6.0.0"
      },
      "funding": {
        "url": "https://github.com/inikulin/parse5?sponsor=1"
      }
    },
    "node_modules/path-exists": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-is-absolute": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-parse": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/picomatch": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/pirates": {
      "version": "4.0.7",
      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/pkg-dir": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "find-up": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/pretty-format": {
      "version": "29.7.0",
      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jest/schemas": "^29.6.3",
        "ansi-styles": "^5.0.0",
        "react-is": "^18.0.0"
      },
      "engines": {
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
    "node_modules/pretty-format/node_modules/ansi-styles": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/prompts": {
      "version": "2.4.2",
      "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "kleur": "^3.0.3",
        "sisteransi": "^1.0.5"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/propagate": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/psl": {
      "version": "1.15.0",
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "punycode": "^2.3.1"
      },
      "funding": {
        "url": "https://github.com/sponsors/lupomontero"
      }
    },
    "node_modules/punycode": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/pure-rand": {
      "version": "6.1.0",
      "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/dubzzz"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/fast-check"
        }
      ],
      "license": "MIT"
    },
    "node_modules/querystringify": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/react-is": {
      "version": "18.3.1",
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/require-directory": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/requires-port": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/resolve": {
      "version": "1.22.11",
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "is-core-module": "^2.16.1",
        "path-parse": "^1.0.7",
        "supports-preserve-symlinks-flag": "^1.0.0"
      },
      "bin": {
        "resolve": "bin/resolve"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/resolve-cwd": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "resolve-from": "^5.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/resolve-from": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/resolve.exports": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/robots-parser": {
      "version": "2.4.0",
      "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-2.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/rrweb-cssom": {
      "version": "0.7.1",
      "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/saxes": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "xmlchars": "^2.2.0"
      },
      "engines": {
        "node": ">=v12.22.7"
      }
    },
    "node_modules/semver": {
      "version": "6.3.1",
      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      }
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/signal-exit": {
      "version": "3.0.7",
      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/sisteransi": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/slash": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/source-map": {
      "version": "0.6.1",
      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/source-map-support": {
      "version": "0.5.13",
      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "buffer-from": "^1.0.0",
        "source-map": "^0.6.0"
      }
    },
    "node_modules/sprintf-js": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause"
    },
    "node_modules/stack-utils": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "escape-string-regexp": "^2.0.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/string-length": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "char-regex": "^1.0.2",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-bom": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-final-newline": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/strip-json-comments": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/supports-preserve-symlinks-flag": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/symbol-tree": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/test-exclude": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "@istanbuljs/schema": "^0.1.2",
        "glob": "^7.1.4",
        "minimatch": "^3.0.4"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/tmp": {
      "version": "0.2.5",
      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=14.14"
      }
    },
    "node_modules/tmpl": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause"
    },
    "node_modules/to-regex-range": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "is-number": "^7.0.0"
      },
      "engines": {
        "node": ">=8.0"
      }
    },
    "node_modules/tough-cookie": {
      "version": "4.1.4",
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "psl": "^1.1.33",
        "punycode": "^2.1.1",
        "universalify": "^0.2.0",
        "url-parse": "^1.5.3"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/tr46": {
      "version": "5.1.1",
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "punycode": "^2.3.1"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/type-detect": {
      "version": "4.0.8",
      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/type-fest": {
      "version": "0.21.3",
      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "(MIT OR CC0-1.0)",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/undici-types": {
      "version": "7.16.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/universalify": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 4.0.0"
      }
    },
    "node_modules/update-browserslist-db": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/browserslist"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/browserslist"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "escalade": "^3.2.0",
        "picocolors": "^1.1.1"
      },
      "bin": {
        "update-browserslist-db": "cli.js"
      },
      "peerDependencies": {
        "browserslist": ">= 4.21.0"
      }
    },
    "node_modules/url-parse": {
      "version": "1.5.10",
      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "querystringify": "^2.1.1",
        "requires-port": "^1.0.0"
      }
    },
    "node_modules/v8-to-istanbul": {
      "version": "9.3.0",
      "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "@jridgewell/trace-mapping": "^0.3.12",
        "@types/istanbul-lib-coverage": "^2.0.1",
        "convert-source-map": "^2.0.0"
      },
      "engines": {
        "node": ">=10.12.0"
      }
    },
    "node_modules/w3c-xmlserializer": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "xml-name-validator": "^5.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/walker": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "makeerror": "1.0.12"
      }
    },
    "node_modules/webidl-conversions": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/whatwg-encoding": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
      "license": "MIT",
      "dependencies": {
        "iconv-lite": "0.6.3"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/whatwg-mimetype": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/whatwg-url": {
      "version": "14.2.0",
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "tr46": "^5.1.0",
        "webidl-conversions": "^7.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/wrap-ansi": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.0.0",
        "string-width": "^4.1.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/write-file-atomic": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "imurmurhash": "^0.1.4",
        "signal-exit": "^3.0.7"
      },
      "engines": {
        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
      }
    },
    "node_modules/ws": {
      "version": "8.19.0",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    },
    "node_modules/xml-name-validator": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/xmlchars": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/y18n": {
      "version": "5.0.8",
      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/yallist": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/yaml": {
      "version": "2.8.2",
      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "bin": {
        "yaml": "bin.mjs"
      },
      "engines": {
        "node": ">= 14.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/eemeli"
      }
    },
    "node_modules/yargs": {
      "version": "17.7.2",
      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "cliui": "^8.0.1",
        "escalade": "^3.1.1",
        "get-caller-file": "^2.0.5",
        "require-directory": "^2.1.1",
        "string-width": "^4.2.3",
        "y18n": "^5.0.5",
        "yargs-parser": "^21.1.1"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/yargs-parser": {
      "version": "21.1.1",
      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/yocto-queue": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    }
  }
}


path: package.json
content: {
  "name": "curated-search",
  "version": "1.0.5",
  "description": "Domain-restricted full-text search for OpenClaw",
  "main": "scripts/search.js",
  "bin": {
    "curated-search": "./scripts/search.js"
  },
  "scripts": {
    "search": "node scripts/search.js",
    "crawl": "node scripts/crawl.js",
    "test": "jest --coverage=false",
    "test:unit": "jest --testPathPattern=unit --coverage",
    "test:integration": "jest --testPathPattern=integration",
    "test:e2e": "node test/e2e/run.js",
    "test:all": "npm run test:unit && npm run test:integration && npm run test:e2e",
    "bench": "node test/benchmarks/search-bench.js"
  },
  "keywords": [
    "openclaw",
    "search",
    "crawler",
    "curated"
  ],
  "author": "Qrusher",
  "license": "MIT",
  "devDependencies": {
    "jest": "^29.7.0",
    "tmp": "^0.2.1",
    "nock": "^13.4.0"
  },
  "dependencies": {
    "jsdom": "^24.0.0",
    "marked": "^12.0.0",
    "minimist": "^1.2.8",
    "minisearch": "^6.3.0",
    "node-fetch": "^2.6.7",
    "robots-parser": "^2.2.1",
    "yaml": "^2.3.4"
  }
}


path: scripts/crawl.js
content: #!/usr/bin/env node
// SECURITY MANIFEST:
//   Environment variables accessed: NONE
//   External endpoints called: User-configured domains from config.yaml during crawling (whitelist only)
//   Local files read: config.yaml
//   Local files written: data/index/*
/**
 * Curated Crawler Runner
 *
 * Entry point for `npm run crawl`.
 * Loads config, initializes indexer, starts crawler.
 */

const fs = require('fs');
const path = require('path');
const yaml = require('yaml');
const CuratedCrawler = require('../src/crawler');
const Indexer = require('../src/indexer');

async function main() {
  try {
    // Load configuration
    const configPath = path.resolve(__dirname, '..', 'config.yaml');
    if (!fs.existsSync(configPath)) {
      console.error(`Configuration not found: ${configPath}`);
      process.exit(1);
    }

    const yamlStr = fs.readFileSync(configPath, 'utf8');
    const config = yaml.parse(yamlStr);

    // Ensure index directory exists
    const indexPath = path.isAbsolute(config.index.path)
      ? config.index.path
      : path.resolve(__dirname, '..', config.index.path);
    const indexDir = path.dirname(indexPath);
    if (!fs.existsSync(indexDir)) {
      fs.mkdirSync(indexDir, { recursive: true });
    }

    // Initialize indexer
    const indexer = new Indexer({ ...config.index, path: indexPath });
    indexer.open();

    // Initialize and start crawler
    const crawler = new CuratedCrawler(config, indexer);
    await crawler.start();

    // Note: crawler.start() calls finalize() which exits the process on completion.
    // But if it rejects, we catch below.
  } catch (error) {
    console.error('Crawler failed:', error);
    process.exit(1);
  }
}

main();


path: scripts/health-check.js
content: #!/usr/bin/env node
// SECURITY MANIFEST:
//   Environment variables accessed: NONE
//   External endpoints called: NONE (checks local index and may invoke local search tool)
//   Local files read: config.yaml, data/index/*
//   Local files written: NONE
/**
 * Health check for Curated Search skill
 *
 * Verifies index exists and search tool returns valid results.
 * Exit 0 = healthy, non-zero = failure.
 */

const { spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const skillDir = path.resolve(__dirname, '..');

function fail(msg) {
  console.error(`[ERROR] ${msg}`);
  process.exit(1);
}

function log(msg) {
  console.log(`[INFO] ${msg}`);
}

// 1. Check index files exist
const indexPath = path.join(skillDir, 'data', 'index.json');
const docsPath = path.join(skillDir, 'data', 'index-docs.json');

if (!fs.existsSync(indexPath)) {
  fail('Index missing: ' + indexPath);
}
if (!fs.existsSync(docsPath)) {
  fail('Documents file missing: ' + docsPath);
}

// 2. Run quick query
try {
  const result = spawnSync(process.execPath || 'node', [
    path.join(skillDir, 'scripts', 'search.js'),
    '--query=test',
    '--limit=1'
  ], {
    cwd: skillDir,
    encoding: 'utf8',
    env: process.env
  });

  if (result.exitCode !== 0) {
    fail('Search tool exited ' + result.exitCode + ': ' + result.stderr);
  }

  const output = JSON.parse(result.stdout);
  if (!Array.isArray(output)) {
    fail('Invalid output format (expected array)');
  }

  log('Index healthy: ' + output.length + ' results returned');
} catch (e) {
  fail('Health check exception: ' + e.message);
}

log('All checks passed');
process.exit(0);


path: scripts/publish-clean.sh
content: #!/bin/bash
# SECURITY MANIFEST:
#   Environment variables accessed: NONE (except those used by clawhub CLI)
#   External endpoints called: ClawHub registry API (publish)
#   Local files read: skill directory, config files
#   Local files written: none
#
# Republish curated-search after remediation
# Run this after rate limit resets (wait ~1-2 hours)

cd /home/q/.openclaw/workspace/skills/curated-search

# Ensure version is 1.0.4 (increment from 1.0.3)
# Update package.json and CHANGELOG.md first if needed

clawhub publish . \
  --slug curated-search \
  --name "Curated Search" \
  --version 1.0.4 \
  --changelog "Security: removed unused search-api.js server component and related references. Cleaned .clawhubignore to exclude all internal audit files." \
  --no-input


path: scripts/search.js
content: #!/usr/bin/env node
// SECURITY MANIFEST:
//   Environment variables accessed: NONE
//   External endpoints called: NONE (search operates on local index only)
//   Local files read: config.yaml, data/index/*
//   Local files written: NONE
/**
 * Curated Search Tool - Phase 4 Implementation
 *
 * Command-line search interface for OpenClaw.
 * Loads index, executes queries, returns JSON results.
 *
 * @version 1.0.0
 */

const fs = require('fs');
const path = require('path');
const yaml = require('yaml');
const MiniSearch = require('minisearch');
const Indexer = require('../src/indexer');

// ============================================================================
// CONSTANTS
// ============================================================================

const VERSION = '1.0.0';
const DEFAULT_CONFIG_PATH = path.resolve(__dirname, '..', 'config.yaml');
const DEFAULT_INDEX_PATH = 'data/index';
const MAX_LIMIT_HARD = 1000; // Internal safety cap
const MAX_QUERY_LENGTH = 1000;
const DEFAULT_LIMIT = 10;

// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================

/**
 * Print help message to stderr
 */
function printHelp() {
  const help = `
Curated Search Tool v${VERSION}
Usage: node scripts/search.js [options] [--query "search terms"]

Options:
  -q, --query <string>       Search query (required unless --stats or --help)
  -l, --limit <number>       Maximum results (default: ${DEFAULT_LIMIT}, max: config.max_limit)
  -d, --domain <string>      Filter to specific domain (e.g., docs.python.org)
  --min-score <number>       Minimum score threshold (0.0 - 1.0)
  --offset <number>         Pagination offset (default: 0)
  --pretty                 Pretty-print JSON output
  --verbose                Show timing and debug information
  --config <path>          Path to config.yaml (default: ${DEFAULT_CONFIG_PATH})
  --stats                  Show index statistics
  --version               Show version information
  --help                  Show this help message

Examples:
  node scripts/search.js --query="python tutorial" --limit=5
  node scripts/search.js "javascript closure" --domain=developer.mozilla.org
  node scripts/search.js --stats

Exit codes:
  0  Success (even if zero results)
  1  Error (missing query, invalid args, etc.)
  2  Configuration error
  3  Index corruption or missing
`;
  console.error(help);
  process.exit(0);
}

/**
 * Print version to stderr
 */
function printVersion() {
  console.error(`Curated Search Tool ${VERSION}`);
  try {
    const pkgPath = path.resolve(__dirname, 'node_modules', 'minisearch', 'package.json');
    const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
    console.error('MiniSearch: ' + pkg.version);
  } catch (e) {
    console.error('MiniSearch: (unable to determine version)');
  }
  process.exit(0);
}

/**
 * Format output JSON (pretty or compact)
 */
function formatOutput(data, pretty = false) {
  if (pretty) {
    return JSON.stringify(data, null, 2);
  }
  return JSON.stringify(data);
}

/**
 * Write error message to stderr and exit
 */
function errorExit(code, details) {
  const errorObj = {
    error: details.code,
    message: details.message,
    ...(details.suggestion && { suggestion: details.suggestion }),
    ...(details.details && { details: details.details })
  };
  console.error(JSON.stringify(errorObj));
  process.exit(code);
}

/**
 * Validate domain format (simple check)
 */
function isValidDomain(domain) {
  // Very basic: should have at least one dot and no spaces
  return typeof domain === 'string' &&
         domain.length > 0 &&
         domain.includes('.') &&
         !domain.includes(' ') &&
         !domain.startsWith('.') &&
         !domain.endsWith('.');
}

/**
 * Get effective limit (apply hard cap)
 */
function getEffectiveLimit(userLimit, configLimit, hardCap = MAX_LIMIT_HARD) {
  const effective = userLimit || configLimit || DEFAULT_LIMIT;
  return Math.min(effective, hardCap);
}

// ============================================================================
// ARGUMENT PARSING
// ============================================================================

// Use minimist for robust parsing
const minimist = require('minimist');

const args = minimist(process.argv.slice(2), {
  string: ['query', 'domain', 'config'],
  number: ['limit', 'min-score', 'offset'],
  boolean: ['pretty', 'verbose', 'stats', 'version', 'help'],
  alias: {
    q: 'query',
    l: 'limit',
    d: 'domain',
    v: 'verbose',
    h: 'help'
  },
  default: {
    limit: DEFAULT_LIMIT,
    'min-score': 0,
    offset: 0
  }
});

// ============================================================================
// EARLY EXIT FLAGS (before heavy work)
// ============================================================================

if (args.help) {
  printHelp();
}

if (args.version) {
  printVersion();
}

// ============================================================================
// LOAD CONFIGURATION
// ============================================================================

let config;
const configPath = args.config || DEFAULT_CONFIG_PATH;

try {
  if (!fs.existsSync(configPath)) {
    errorExit(2, {
      code: 'config_missing',
      message: `Configuration file not found: ${configPath}`,
      suggestion: 'Ensure you are running from the skill directory or specify --config path'
    });
  }

  const yamlStr = fs.readFileSync(configPath, 'utf8');
  config = yaml.parse(yamlStr);
} catch (e) {
  errorExit(2, {
    code: 'config_invalid',
    message: `Failed to parse configuration: ${e.message}`,
    suggestion: 'Check YAML syntax in config.yaml',
    details: { path: configPath, error: e.message }
  });
}

// Config sanity checks
if (!config.index || !config.index.path) {
  errorExit(2, {
    code: 'config_missing_index_path',
    message: 'Configuration missing index.path',
    suggestion: 'Add index.path to config.yaml'
  });
}

if (!config.search) {
  config.search = {};
}

// ============================================================================
// VALIDATION
// ============================================================================

const validationErrors = [];

// Query validation
if (!args.stats && !args.query) {
  validationErrors.push({
    code: 'missing_query',
    message: 'Search query is required (use --query or positional argument)',
    suggestion: 'curated-search --query="python" or curated-search "python"'
  });
}

if (args.query && typeof args.query === 'string' && args.query.length > MAX_QUERY_LENGTH) {
  validationErrors.push({
    code: 'query_too_long',
    message: `Query exceeds ${MAX_QUERY_LENGTH} character limit`,
    suggestion: 'Shorten your query',
    details: { length: args.query.length }
  });
}

// Limit validation
const effectiveLimit = getEffectiveLimit(args.limit, config.search.default_limit);
if (args.limit && args.limit > (config.search.max_limit || 100)) {
  validationErrors.push({
    code: 'limit_exceeded',
    message: `Limit cannot exceed ${config.search.max_limit || 100}`,
    suggestion: `Use --limit <= ${config.search.max_limit || 100}`
  });
}

// Domain validation
if (args.domain && !isValidDomain(args.domain)) {
  validationErrors.push({
    code: 'invalid_domain',
    message: `Invalid domain format: ${args.domain}`,
    suggestion: 'Use format like "docs.python.org" (no protocol)'
  });
}

// Mutually exclusive flags
if (args.stats && args.query) {
  validationErrors.push({
    code: 'conflicting_flags',
    message: 'Cannot use --stats with --query',
    suggestion: 'Use --stats alone to show index statistics'
  });
}

// Report validation errors
if (validationErrors.length > 0) {
  errorExit(1, validationErrors[0]);
}

// ============================================================================
// INITIALIZE INDEXER
// ============================================================================

let indexer;
try {
  // Resolve index path (relative to skill directory)
  const indexPath = path.isAbsolute(config.index.path)
    ? config.index.path
    : path.resolve(__dirname, '..', config.index.path);

  // Check index file exists
  const indexFile = indexPath + '.json';
  if (!fs.existsSync(indexFile)) {
    errorExit(3, {
      code: 'index_not_found',
      message: 'Search index not found. The index has not been built yet.',
      suggestion: 'Run the crawler first: npm run crawl',
      details: { path: indexFile }
    });
  }

  // Create indexer with resolved path
  const indexerConfig = { ...config.index, path: indexPath };
  indexer = new Indexer(indexerConfig);

  const loaded = indexer.open();
  // Note: open() returns false if it created a fresh index (no existing data)
  // That's okay for search (we'll get zero results)
} catch (e) {
  // Distinguish corruption errors
  if (e.message && e.message.includes('loadJSON')) {
    errorExit(3, {
      code: 'index_corrupted',
      message: 'Index file is corrupted or incompatible with this version',
      suggestion: 'Rebuild the index by running: npm run crawl',
      details: { error: e.message }
    });
  }

  console.error(JSON.stringify({
    error: 'index_init_failed',
    message: `Failed to initialize index: ${e.message}`,
    suggestion: 'Check index files and permissions'
  }));
  process.exit(1);
}

// ============================================================================
// HANDLE --stats MODE
// ============================================================================

if (args.stats) {
  try {
    const stats = indexer.getStats();
    const output = {
      documents: stats.documents,
      domains: stats.domain_breakdown ? Object.keys(stats.domain_breakdown).length : stats.domains,
      domain_breakdown: stats.domain_breakdown || {},
      oldest_crawl: stats.oldest_crawl ? new Date(stats.oldest_crawl).toISOString() : null,
      newest_crawl: stats.newest_crawl ? new Date(stats.newest_crawl).toISOString() : null,
      index_path: config.index.path
    };
    console.log(formatOutput(output, args.pretty));
    indexer.close();
    process.exit(0);
  } catch (e) {
    errorExit(1, {
      code: 'stats_failed',
      message: `Failed to get statistics: ${e.message}`
    });
  }
}

// ============================================================================
// PERFORM SEARCH
// ============================================================================

const startTime = Date.now();

try {
  // Prepare search options
  const searchOptions = {
    limit: effectiveLimit,
    offset: args.offset || 0,
    minScore: args['min-score'] !== undefined ? args['min-score'] : (config.search.min_score || 0)
  };

  if (args.domain) {
    searchOptions.domainFilter = args.domain;
  }

  // Execute search
  const result = indexer.search(args.query, searchOptions);

  // Build response
  const response = {
    query: args.query,
    total: result.total,
    results: result.results,
    took_ms: Date.now() - startTime,
    limit: effectiveLimit,
    offset: searchOptions.offset
  };

  // Output on stdout
  console.log(formatOutput(response, args.pretty));

  // Verbose logging to stderr
  if (args.verbose) {
    console.error(`[INFO] Search completed in ${response.took_ms}ms`);
    console.error(`[INFO] Query: "${args.query}"`);
    console.error(`[INFO] Options: ${JSON.stringify(searchOptions)}`);
    console.error(`[INFO] Found ${result.total} results, returning ${result.results.length} (limit ${effectiveLimit})`);
  }

  indexer.close();
  process.exit(0);

} catch (e) {
  console.error(JSON.stringify({
    error: 'search_failed',
    message: `Search execution failed: ${e.message}`,
    suggestion: 'Check query syntax and index integrity',
    details: { query: args.query, stack: args.verbose ? e.stack : undefined }
  }));
  indexer.close();
  process.exit(1);
}


path: scripts/validate-config.js
content: #!/usr/bin/env node
// SECURITY MANIFEST:
//   Environment variables accessed: NONE
//   External endpoints called: NONE
//   Local files read: config.yaml
//   Local files written: NONE
/**
 * Config Validator
 *
 * Validates config.yaml for:
 * - Required fields present
 * - Domain/seeds consistency
 * - Value ranges reasonable
 * - Seed URLs match whitelisted domains
 */

const fs = require('fs');
const path = require('path');
const yaml = require('yaml');

// Parse --config argument to override default config path
let configPath = path.resolve(__dirname, '..', 'config.yaml');
const args = process.argv.slice(2);
const configIdx = args.indexOf('--config');
if (configIdx !== -1 && args[configIdx + 1]) {
  configPath = path.resolve(args[configIdx + 1]);
}

const errors = [];
const warnings = [];

function validate() {
  console.log('Validating configuration...\n');

  // Check file exists
  if (!fs.existsSync(configPath)) {
    errors.push(`Config file not found: ${configPath}`);
    return;
  }

  // Parse YAML
  let config;
  try {
    const yamlStr = fs.readFileSync(configPath, 'utf8');
    config = yaml.parse(yamlStr);
  } catch (e) {
    errors.push(`YAML parse error: ${e.message}`);
    return;
  }

  // Required top-level sections
  const required = ['domains', 'seeds', 'crawl', 'index', 'search'];
  for (const key of required) {
    if (!config[key]) {
      errors.push(`Missing required section: ${key}`);
    }
  }

  // Validate domains
  if (config.domains) {
    if (!Array.isArray(config.domains) || config.domains.length === 0) {
      errors.push('domains must be a non-empty array');
    } else {
      // Check for duplicates
      const unique = new Set(config.domains);
      if (unique.size !== config.domains.length) {
        warnings.push('Duplicate domains detected');
      }

      // Validate domain format (simple check)
      config.domains.forEach((domain, i) => {
        if (!domain.includes('.')) {
          errors.push(`Invalid domain format at index ${i}: ${domain}`);
        }
        if (domain.startsWith('http://') || domain.startsWith('https://')) {
          errors.push(`Domain should NOT include protocol, remove 'https://': ${domain}`);
        }
      });
    }
  }

  // Validate seeds
  if (config.seeds) {
    if (!Array.isArray(config.seeds) || config.seeds.length === 0) {
      errors.push('seeds must be a non-empty array');
    } else {
      // Check each seed URL matches a whitelisted domain
      if (config.domains) {
        config.seeds.forEach((seed, i) => {
          try {
            const url = new URL(seed);
            const hostname = url.hostname;
            
            // Check if this hostname or its parent domain is whitelisted
            const isWhitelisted = config.domains.some(domain => {
              return hostname === domain || hostname.endsWith('.' + domain);
            });
            
            if (!isWhitelisted) {
              errors.push(`Seed ${i} hostname '${hostname}' not in domain whitelist: ${seed}`);
            }
          } catch (e) {
            errors.push(`Invalid URL at seeds[${i}]: ${seed}`);
          }
        });
      }
    }
  }

  // Validate crawl settings
  if (config.crawl) {
    const c = config.crawl;
    
    if (typeof c.depth !== 'number' || c.depth < 0 || c.depth > 5) {
      errors.push('crawl.depth must be a number between 0 and 5');
    }
    
    if (typeof c.delay !== 'number' || c.delay < 0) {
      errors.push('crawl.delay must be a non-negative number (milliseconds)');
    }
    
    if (typeof c.timeout !== 'number' || c.timeout < 1) {
      errors.push('crawl.timeout must be a positive number (seconds)');
    }
    
    if (typeof c.max_documents !== 'number' || c.max_documents < 1) {
      errors.push('crawl.max_documents must be a positive number');
    }
    
    if (typeof c.concurrent_requests !== 'number' || c.concurrent_requests < 1) {
      errors.push('crawl.concurrent_requests must be at least 1');
    }
    
    // Warnings for aggressive settings
    if (c.delay < 500) {
      warnings.push(`crawl.delay is very low (${c.delay}ms) - risk of being blocked`);
    }
    
    if (c.concurrent_requests > 3) {
      warnings.push(`crawl.concurrent_requests is high (${c.concurrent_requests})`);
    }
    
    if (c.depth > 3) {
      warnings.push(`crawl.depth is high (${c.depth}) - will crawl extensively`);
    }
  }

  // Validate index settings
  if (config.index) {
    if (!config.index.path) {
      errors.push('index.path is required');
    }
    
    if (config.index.auto_save_after !== undefined) {
      if (typeof config.index.auto_save_after !== 'number' || config.index.auto_save_after < 0) {
        errors.push('index.auto_save_after must be a non-negative number');
      }
    }
    
    if (config.index.min_content_length !== undefined && config.index.min_content_length < 50) {
      warnings.push('index.min_content_length is very low - may index navigation pages');
    }
  }

  // Validate search settings
  if (config.search) {
    if (config.search.default_limit !== undefined) {
      const dl = config.search.default_limit;
      if (typeof dl !== 'number' || dl < 1 || dl > 100) {
        errors.push('search.default_limit must be between 1 and 100');
      }
    }
    
    if (config.search.max_limit !== undefined) {
      const ml = config.search.max_limit;
      if (typeof ml !== 'number' || ml < 1) {
        errors.push('search.max_limit must be a positive number');
      }
    }

    // Cross-field validation: default_limit <= max_limit
    if (config.search.default_limit !== undefined && config.search.max_limit !== undefined) {
      if (config.search.default_limit > config.search.max_limit) {
        errors.push('search.default_limit cannot exceed search.max_limit');
      }
    }
  }

  // Report results
  console.log('--- Validation Results ---\n');
  
  if (errors.length === 0 && warnings.length === 0) {
    console.log('✅ Configuration is valid!\n');
    console.log(`Domains configured: ${config.domains.length}`);
    console.log(`Seed URLs configured: ${config.seeds.length}`);
    console.log(`Max crawl depth: ${config.crawl.depth}`);
    console.log(`Crawl delay: ${config.crawl.delay}ms`);
    console.log(`Max documents: ${config.crawl.max_documents}`);
    return 0;
  }

  if (errors.length > 0) {
    console.error(`❌ ERRORS (${errors.length}):\n`);
    errors.forEach(e => console.error(`  • ${e}`));
    console.error('');
  }
  
  if (warnings.length > 0) {
    console.error(`⚠️  WARNINGS (${warnings.length}):\n`);
    warnings.forEach(w => console.error(`  • ${w}`));
    console.error('');
  }

  return errors.length > 0 ? 1 : 0;
}

const exitCode = validate();
process.exit(exitCode);


path: skill.yaml
content: # Curated Search Skill Manifest
# OpenClaw skill definition

name: "curated-search"
version: "1.0.5"
description: "Domain-restricted full-text search over curated technical documentation"
author: "Qrusher"
license: "MIT"

# Tool definitions
tools:
  - name: "search"
    description: "Search the curated index for technical documentation"
    parameters:
      - name: "query"
        type: "string"
        required: true
        description: "Search query terms"
      - name: "limit"
        type: "number"
        required: false
        default: 5
        description: "Maximum results (capped by config.max_limit)"
      - name: "domain"
        type: "string"
        required: false
        description: "Filter to specific domain (e.g., docs.python.org)"
      - name: "min_score"
        type: "number"
        required: false
        default: 0.0
        description: "Minimum relevance score (0.0-1.0)"
      - name: "offset"
        type: "number"
        required: false
        default: 0
        description: "Pagination offset"
    command: "node scripts/search.js"
    return_type: "json"
    timeout: 30
    success_exit_codes: [0]

# Dependencies (informational - not auto-installed by OpenClaw)
requirements:
  - "minisearch"
  - "minimist"
  - "yaml"
  - "robots-parser"
  - "node-fetch"
  - "jsdom"

# Setup instructions (for manual installation)
setup:
  - "npm install"
  - "npm run crawl   # optional: build initial index"
  - "echo 'Skill is ready once index is built'"

# Optional: config defaults can be overridden per-install
config:
  index:
    path: "data/index"
  search:
    default_limit: 10
    max_limit: 100
    min_score: 0.0


path: src/content-extractor.js
content: /**
 * Content Extractor - Phase 3.2 Implementation
 *
 * Converts raw HTML into clean, searchable text content.
 * Implements full pipeline: type detection, boilerplate removal, title extraction,
 * content extraction with code preservation, excerpt generation, quality validation.
 */

const { JSDOM } = require('jsdom');
const { marked } = require('marked');

class ContentExtractor {
  constructor(config) {
    this.config = config;
    this.minContentLength = config.content?.min_content_length || 500;
    this.maxContentLength = config.content?.max_content_length || 50000;
  }

  // ============================================================================
  // MAIN ENTRY POINT
  // ============================================================================

  /**
   * Extract content from HTML
   * @param {string} html - Raw HTML
   * @param {string} url - Source URL
   * @returns {Object} { title, content, excerpt, quality, type, issues }
   */
  async extract(html, url) {
    const dom = new JSDOM(html, { url });
    const doc = dom.window.document;

    // 1. Detect content type
    const type = this.detectContentType(doc, url);

    // 2. Remove boilerplate (clone to avoid mutating original)
    const cleanedDoc = this.removeBoilerplate(doc.cloneNode(true));

    // 3. Extract title with priority
    const title = this.extractTitle(cleanedDoc, url);

    // 4. Extract main content based on type
    let content = await this.extractContent(cleanedDoc, type, url);

    if (!content || content.length < 100) {
      // Fallback: body with boilerplate removal
      content = this.extractBodyFallback(cleanedDoc);
    }

    // 5. Normalize content (whitespace, code preservation)
    content = this.normalizeContent(content);

    // 6. Truncate if too long (reserve space for ellipsis)
    if (content.length > this.maxContentLength) {
      const ellipsis = '...';
      const max = this.maxContentLength;
      // Ensure we leave room for ellipsis
      const truncateLen = Math.max(max - ellipsis.length, 0);
      content = content.substring(0, truncateLen).trim() + ellipsis;
    }

    // 7. Generate excerpt (without code blocks)
    const excerpt = this.generateExcerpt(content);

    // 8. Quality validation
    const validation = this.validateQuality(content, title, url);

    return {
      title,
      content,
      excerpt,
      quality: validation.quality,
      type,
      issues: validation.issues,
      url
    };
  }

  // ============================================================================
  // 1. CONTENT TYPE DETECTION
  // ============================================================================

  detectContentType(doc, url) {
    try {
      const urlObj = new URL(url);
      const hostname = urlObj.hostname;

      // Order matters: check special hosts first
      if (hostname === 'raw.githubusercontent.com') {
        return 'github_markdown';
      }

      if (hostname === 'stackprinter.appspot.com') {
        return 'stackprinter';
      }

      // Site-specific map
      const siteMap = {
        'developer.mozilla.org': 'mdn',
        'docs.python.org': 'python_docs',
        'nodejs.org': 'node_docs',
        'en.wikipedia.org': 'wikipedia',
        'github.com': 'github_html',
        'stackoverflow.com': 'stackoverflow',
        'man7.org': 'manpage',
        'linux.die.net': 'manpage',
        'docs.openclaw.ai': 'generic'
      };

      return siteMap[hostname] || 'generic';
    } catch (e) {
      return 'generic';
    }
  }

  // ============================================================================
  // 2. BOILERPLATE REMOVAL
  // ============================================================================

  removeBoilerplate(element) {
    // Elements that should always be removed
    const selectors = [
      // Navigation
      'nav', 'header', 'footer',
      '[role="navigation"]', '[role="banner"]', '[role="contentinfo"]',
      '.navbar', '.nav', '.menu', '.breadcrumb', '.breadcrumbs',
      // Sidebars
      'aside', '.sidebar', '.side-bar', '[role="complementary"]',
      // Ads & promos
      '.ad', '.advertisement', '.ads', '.advert',
      '[class*="advert"]', '[id*="ad"]',
      // UI elements
      '.cookie-banner', '.cookie-consent', '.gdpr',
      '.newsletter-signup', '.subscribe',
      '.social-share', '.share-buttons',
      '.comment-form', '#comments', '.discussion',
      // Metadata areas
      '.meta', '.metadata', '.byline',
      '.author-info', '.post-meta',
      '.published-date', '.modified-date',
      // Related content (often duplicate)
      '.related-articles', '.read-more', '.see-also',
      '.similar-pages', '.recommended', '.popular-posts',
      // Scripts, styles, media
      'script', 'style', 'noscript',
      'iframe', 'object', 'embed', 'video', 'audio',
      // Hidden
      '[hidden]',
      '[style*="display: none"]',
      '[style*="visibility: hidden"]',
      '[aria-hidden="true"]'
    ];

    selectors.forEach(selector => {
      try {
        element.querySelectorAll(selector).forEach(el => el.remove());
      } catch (e) {
        // ignore invalid selectors
      }
    });

    return element;
  }

  // ============================================================================
  // 3. TITLE EXTRACTION (Priority Order)
  // ============================================================================

  extractTitle(doc, url) {
    const hostname = new URL(url).hostname;

    // 1. <title> tag
    let title = doc.querySelector('title')?.textContent.trim() || '';

    // 2. <h1> if exactly one
    if (!title) {
      const h1s = doc.querySelectorAll('h1');
      if (h1s.length === 1) {
        title = h1s[0].textContent.trim();
      }
    }

    // 3. OpenGraph meta
    if (!title) {
      const og = doc.querySelector('meta[property="og:title"]');
      if (og) title = og.getAttribute('content')?.trim() || '';
    }

    // 4. Twitter meta
    if (!title) {
      const tw = doc.querySelector('meta[name="twitter:title"]');
      if (tw) title = tw.getAttribute('content')?.trim() || '';
    }

    // 5. First <h2>
    if (!title) {
      const h2 = doc.querySelector('h2');
      if (h2) title = h2.textContent.trim();
    }

    // 6. First sentence of content (fallback)
    if (!title) {
      const bodyText = doc.body?.textContent.trim() || '';
      // Special case for raw GitHub markdown: use first line as title (before newline)
      if (hostname.includes('raw.githubusercontent.com')) {
        // Take first non-empty line; if it starts with '#', strip only the leading '#'
        const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
        const firstLine = lines[0] || '';
        // Only strip leading '#', preserve rest (e.g., "Node.js")
        title = firstLine.replace(/^#+\s*/, '').trim();
      } else {
        const firstSentence = bodyText.split(/[.!?]/)[0];
        title = firstSentence.substring(0, 150);
      }
    }

    // Clean title: remove site suffixes
    title = this.cleanTitle(title, hostname);

    // Length limit
    if (title.length > 150) {
      title = title.substring(0, 147) + '...';
    }

    return title.trim();
  }

  cleanTitle(title, hostname) {
    if (!title) return '';

    // Site-specific suffix patterns
    const suffixPatterns = [
      new RegExp(` - ${hostname}$`, 'i'),
      new RegExp(` \\| ${hostname}$`, 'i'),
      new RegExp(` — ${hostname}$`, 'i'),
      new RegExp(` :: ${hostname}$`, 'i'),
      / - Documentation$/i,
      / - MDN$/i,
      / - Wikipedia$/i,
      / · GitHub$/i,
      / \| Docs$/i
    ];

    for (const pattern of suffixPatterns) {
      title = title.replace(pattern, '');
    }

    return title.trim();
  }

  // ============================================================================
  // 4. CONTENT EXTRACTION BY TYPE
  // ============================================================================

  async extractContent(doc, type, url) {
    switch (type) {
      case 'mdn':
        return this.extractMDN(doc);
      case 'python_docs':
        return this.extractPythonDocs(doc);
      case 'wikipedia':
        return this.extractWikipedia(doc);
      case 'github_html':
        return this.extractGitHubHTML(doc);
      case 'github_markdown':
        return this.extractGitHubMarkdown(doc, url);
      case 'stackprinter':
        return this.extractStackPrinter(doc);
      case 'manpage':
        return this.extractManPage(doc);
      case 'generic':
      default:
        return this.extractGeneric(doc);
    }
  }

  extractMDN(doc) {
    const selectors = [
      'article.main-page-content',
      '#content article',
      'article'
    ];

    const el = this.selectFirst(doc, selectors);
    if (!el) return null;

    // MDN-specific cleanup
    el.querySelectorAll('aside.interactive, .document-toc').forEach(e => e.remove());

    return this.elementToText(el);
  }

  extractPythonDocs(doc) {
    const selectors = [
      '.body',
      'div.document',
      'div.bodywrapper > div.body',
      '[role="main"]'
    ];
    const el = this.selectFirst(doc, selectors);
    return el ? this.elementToText(el) : null;
  }

  extractWikipedia(doc) {
    const selectors = [
      '#mw-content-text',
      '.mw-parser-output'
    ];
    const el = this.selectFirst(doc, selectors);
    if (!el) return null;

    // Remove infobox, navbox, See also
    el.querySelectorAll('.infobox, .navbox, #See_also').forEach(e => {
      const section = e.closest('div, section');
      if (section) section.remove();
      else e.remove();
    });

    return this.elementToText(el);
  }

  extractGitHubHTML(doc) {
    const selectors = [
      '.markdown-body',
      '.repository-content .Box'
    ];
    const el = this.selectFirst(doc, selectors);
    return el ? this.elementToText(el) : null;
  }

  extractGitHubMarkdown(doc, url) {
    // Raw markdown from raw.githubusercontent.com: keep as plain text to preserve code fences
    const markdown = doc.body?.textContent.trim() || '';
    return markdown || null;
  }

  extractStackPrinter(doc) {
    // StackPrinter pages: entire body is Q&A text
    const body = doc.body?.textContent.trim() || '';
    // Could parse into separate docs, but for now return whole body
    return body;
  }

  extractManPage(doc) {
    // Find <pre> with man page content
    const pre = doc.querySelector('pre.manpage, pre.man-text, pre');
    if (!pre) return null;

    let text = pre.textContent;

    // Strip headers/footers
    text = text.replace(/^Manual page.*$/gm, '');
    text = text.replace(/^Linux man-pages.*$/gm, '');
    text = text.replace(/^\s*Page \d+.*$/gm, '');

    // Extract title from NAME section
    const nameMatch = text.match(/NAME\s+(.+?)\s+-\s+(.+)/s);
    if (nameMatch) {
      // Title will be set separately; content includes full man page
    }

    return text.trim();
  }

  extractGeneric(doc) {
    const selectors = [
      'article',
      'main',
      '[role="main"]',
      '.content',
      '.main-content',
      '#content',
      '.documentation',
      'section',
      '.body',
      '.post-content',
      'div.content-wrapper',
      'div.container > div.row'
    ];

    const el = this.selectFirst(doc, selectors);
    if (el && el.textContent.length > 500) {
      return this.elementToText(el);
    }

    // Fallback: text density scoring
    return this.extractByTextDensity(doc);
  }

  selectFirst(doc, selectors) {
    for (const selector of selectors) {
      const el = doc.querySelector(selector);
      if (el && el.textContent.trim().length > 500) {
        return el;
      }
    }
    return null;
  }

  extractByTextDensity(doc) {
    const candidates = doc.querySelectorAll('div, article, section');
    let bestElement = null;
    let bestScore = -Infinity;

    for (const element of candidates) {
      const score = this.scoreElement(element);
      if (score > bestScore) {
        bestScore = score;
        bestElement = element;
      }
    }

    if (bestElement && bestScore > 0.5) {
      return this.elementToText(bestElement);
    }

    return null;
  }

  scoreElement(element) {
    const text = element.textContent.trim();
    const textLength = text.length;

    // Count child elements (excluding script/style)
    const tags = element.querySelectorAll('*:not(script):not(style):not(svg)');
    const tagCount = tags.length;

    // Link density
    const links = element.querySelectorAll('a');
    const linkCount = links.length;
    const linkDensity = linkCount / (textLength / 100 + 1);

    // Score: more text, fewer tags, lower link density
    return textLength / (tagCount + linkDensity * 20 + 1);
  }

  extractBodyFallback(doc) {
    const body = doc.body?.cloneNode(true) || doc.documentElement.cloneNode(true);

    // Remove obvious boilerplate
    const removals = ['nav', 'header', 'footer', 'aside', '.sidebar', 'script', 'style', '.ad'];
    removals.forEach(sel => {
      try {
        body.querySelectorAll(sel).forEach(el => el.remove());
      } catch (e) {}
    });

    return body.textContent.trim().replace(/\s+/g, ' ');
  }

  // ============================================================================
  // 5. TEXT NORMALIZATION & CODE PRESERVATION
  // ============================================================================

  normalizeContent(content) {
    if (!content) return '';

    // We need to preserve code blocks. Assuming content may already have
    // ``` markers from elementToText, we only collapse whitespace outside code.
    // Split by code blocks, normalize text parts, reassemble.

    const parts = content.split(/(```[\s\S]*?```)/g);

    return parts.map(part => {
      if (part.startsWith('```') && part.endsWith('```')) {
        return part; // Leave code blocks untouched
      }
      // Normalize whitespace in text
      return part
        .replace(/\s+/g, ' ')           // Collapse whitespace
        .replace(/\n\s*\n/g, '\n')      // Preserve paragraph breaks
        .replace(/[\u200B-\u200D\uFEFF]/g, ''); // Remove zero-width spaces
    }).join('').trim();
  }

  elementToText(element) {
    let content = '';

    function traverse(node) {
      if (node.nodeType === 3) {
        // Text node
        content += node.textContent;
      } else if (node.nodeType === 1) {
        // Element node
        const tag = node.tagName.toLowerCase();

        if (tag === 'pre') {
          // Code block
          const code = node.textContent;
          content += '\n```\n' + code.trim() + '\n```\n';
        } else if (tag === 'code') {
          // Inline code (if not inside pre)
          const parent = node.parentElement?.tagName.toLowerCase();
          if (parent !== 'pre') {
            content += '`' + node.textContent.trim() + '`';
          }
        } else if (tag === 'br') {
          content += '\n';
        } else {
          // Recurse
          node.childNodes.forEach(traverse);
        }
      }
    }

    traverse(element);
    return content.trim();
  }

  // ============================================================================
  // 6. EXCERPT GENERATION
  // ============================================================================

  generateExcerpt(content, maxLength = 200) {
    if (!content) return '';

    // Remove code blocks for excerpt
    const textOnly = content.replace(/```[\s\S]*?```/g, '');

    if (textOnly.length <= maxLength) {
      return textOnly;
    }

    // Truncate to maxLength
    let truncated = textOnly.substring(0, maxLength);

    // Find last sentence boundary
    const lastPeriod = truncated.lastIndexOf('. ');
    const lastQuestion = truncated.lastIndexOf('? ');
    const lastExclamation = truncated.lastIndexOf('! ');

    const cutoff = Math.max(lastPeriod, lastQuestion, lastExclamation);

    if (cutoff > maxLength * 0.5) {
      return truncated.substring(0, cutoff + 1) + '...';
    }

    // Otherwise cut at word boundary
    const lastSpace = truncated.lastIndexOf(' ');
    if (lastSpace > maxLength * 0.7) {
      return truncated.substring(0, lastSpace) + '...';
    }

    return truncated + '...';
  }

  // ============================================================================
  // 7. QUALITY VALIDATION
  // ============================================================================

  validateQuality(content, title, url) {
    const issues = [];
    const contentLower = (content + ' ' + title).toLowerCase();

    // Length check
    if (content.length < this.minContentLength) {
      issues.push({
        level: 'warn',
        code: 'content_too_short',
        length: content.length,
        message: `Content too short (${content.length} < ${this.minContentLength})`
      });
    }

    // Error page detection
    const ERROR_PATTERNS = [
      /page not found/i,
      /404 not found/i,
      /error 404/i,
      /content not available/i,
      /access denied/i,
      /login required/i,
      /sign in to/i,
      /this page does not exist/i,
      /the page you requested was not found/i,
      /we couldn't find that page/i
    ];

    if (ERROR_PATTERNS.some(pattern => pattern.test(contentLower))) {
      issues.push({
        level: 'error',
        code: 'error_page',
        message: 'Detected error page patterns'
      });
    }

    // Stub detection
    const STUB_PATTERNS = [
      /this article is a stub/i,
      /this section needs expansion/i,
      /help improve this article/i,
      /placeholder page/i,
      /to be written/i,
      /coming soon/i,
      /under construction/i
    ];

    if (STUB_PATTERNS.some(pattern => pattern.test(contentLower))) {
      issues.push({
        level: 'warn',
        code: 'stub_content',
        message: 'Detected stub/placeholder content'
      });
    }

    // Link density
    const linkCount = (content.match(/\[/g) || []).length; // approximate markdown links? Actually we should count <a> but content is plain text now
    // Better: compute from original element before conversion
    // For now, skip link density (needs original element)

    // Determine overall quality
    let quality = 'high';
    if (issues.some(i => i.level === 'error')) {
      quality = 'low';
    } else if (issues.some(i => i.level === 'warn')) {
      quality = 'medium';
    }

    return {
      quality,
      issues
    };
  }
}

module.exports = ContentExtractor;


path: src/crawler.js
content: /**
 * Curated Crawler - Enhanced with Orchestration (Phase 3.4)
 *
 * Domain-restricted web crawler with politeness, robots.txt, state persistence,
 * checkpoint/resume, signal handling, health monitoring, and graceful shutdown.
 *
 * Architecture:
 *   State machine: IDLE → RUNNING → STOPPING → STOPPED
 *   Persistence: saves queue, seen set, stats every N docs + time interval
 *   Health: windowed error rate, recent success tracking
 */

// Use global fetch (Node 18+) directly; tests may mock it.
const fs = require('fs');
const path = require('path');
const RobotsParser = require('robots-parser');
const ContentExtractor = require('./content-extractor');
const urlNormalizer = require('./url-normalizer');
const RateLimiter = require('./rate-limiter');

class CuratedCrawler {
  constructor(config, indexer) {
    this.config = config;
    this.indexer = indexer;
    this.log = this.createLogger();
    this.extractor = new ContentExtractor(config);
    this.rateLimiter = new RateLimiter(config, this); // 'this' provides getRobotsParser

    // State file path
    this.statePath = path.resolve(__dirname, '..', 'data', 'crawl-state.json');
    this.stateDir = path.dirname(this.statePath);
    if (!fs.existsSync(this.stateDir)) {
      fs.mkdirSync(this.stateDir, { recursive: true });
    }

    // Load or initialize state
    this.state = this.loadState();

    // Queue and seen derived from state
    this.queue = this.state.queue ? [...this.state.queue] : [];
    this.seen = new Set(this.state.seen ? Object.keys(this.state.seen) : []);

    // Stats (merge persisted stats with runtime)
    this.stats = { ...this.state.stats, recentErrors: [], recentRequests: 0, recentErrorCount: 0 };

    this.robotsCache = new Map();
    this.running = false;
    this.startTime = null;
    this.lastCheckpoint = Date.now();

    // Shutdown handling
    this.shutdownRequested = false;
    this.shutdownReason = null;
    this.setupSignalHandlers();

    // Checkpoint configuration
    this.checkpointInterval = 5 * 60 * 1000; // 5 minutes
    this.autoSaveAfter = this.config.index?.auto_save_after || 100;
  }

  createLogger() {
    const levels = { debug: 0, info: 1, warn: 2, error: 3 };
    const level = levels[this.config.logging?.level || 'info'] || 1;

    return (msg, lvl = 'info', meta = {}) => {
      if (levels[lvl] >= level) {
        const timestamp = new Date().toISOString();
        const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
        console.log(`[${timestamp}] [${lvl.toUpperCase()}] ${msg}${metaStr}`);
      }
    };
  }

  // ============================================================================
  // STATE PERSISTENCE
  // ============================================================================

  loadState() {
    if (!fs.existsSync(this.statePath)) {
      this.log('No saved state found, starting fresh', 'info');
      return this.createFreshState();
    }

    try {
      const raw = fs.readFileSync(this.statePath, 'utf8');
      const state = JSON.parse(raw);
      this.log(`Resuming from state: ${state.stats?.documentsIndexed || 0} docs indexed, ${state.queue?.length || 0} queued`, 'info');
      return state;
    } catch (e) {
      this.log(`Corrupted state file, starting fresh: ${e.message}`, 'warn');
      fs.renameSync(this.statePath, this.statePath + '.corrupt');
      return this.createFreshState();
    }
  }

  createFreshState() {
    return {
      version: 1,
      createdAt: Date.now(),
      updatedAt: Date.now(),
      queue: [],
      seen: {},
      failed: {},
      robotsCache: {},
      stats: {
        started: Date.now(),
        documentsIndexed: 0,
        requestsMade: 0,
        urlsExtracted: 0,
        errors: 0,
        robotsBlocked: 0,
        rejectedDocuments: 0,
        maxDepthReached: 0,
        domainBreakdown: {}
      },
      lastCheckpoint: Date.now()
    };
  }

  saveState() {
    try {
      // Truncate queue and seen to prevent unbounded growth
      const truncatedQueue = this.queue.slice(0, 10000);
      const seenEntries = Array.from(this.seen).map(url => ({ url, seenAt: Date.now() }));
      const truncatedSeen = seenEntries.slice(0, 50000);

      const state = {
        ...this.state,
        queue: truncatedQueue.map(item => ({ url: item.url, depth: item.depth })),
        seen: Object.fromEntries(truncatedSeen.map(entry => [entry.url, entry.seenAt])),
        failed: this.state.failed, // keep as-is (could truncate)
        robotsCache: this.robotsCache, // Could truncate to recent 100
        stats: this.stats,
        updatedAt: Date.now(),
        lastCheckpoint: Date.now()
      };

      const tempPath = this.statePath + '.tmp';
      fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
      fs.renameSync(tempPath, this.statePath);

      this.lastCheckpoint = Date.now();
      this.log(`Checkpoint saved: queue=${this.queue.length}, seen=${this.seen.size}, indexed=${this.stats.documentsIndexed}`, 'debug');
    } catch (e) {
      this.log(`Failed to save checkpoint: ${e.message}`, 'error');
    }
  }

  // ============================================================================
  // SIGNAL HANDLING
  // ============================================================================

  setupSignalHandlers() {
    process.on('SIGINT', this.handleSignal.bind(this, 'SIGINT'));
    process.on('SIGTERM', this.handleSignal.bind(this, 'SIGTERM'));
  }

  handleSignal(signal) {
    if (this.shutdownRequested) {
      this.log('Force exit (second signal)', 'warn');
      process.exit(1);
    }

    this.shutdownRequested = true;
    this.shutdownReason = signal;
    this.log(`Received ${signal}, initiating graceful shutdown...`, 'info');
    this.stop();
  }

  // ============================================================================
  // HEALTH MONITORING
  // ============================================================================

  updateHealthMetrics(success = false) {
    const now = Date.now();
    this.stats.recentRequests++;

    if (success) {
      this.stats.lastSuccessTime = now;
      this.stats.recentErrorCount = 0;
      this.stats.recentErrors = [];
    } else {
      this.stats.recentErrorCount++;
      this.stats.recentErrors.push({ time: now });
    }

    // Keep only last 100 requests
    if (this.stats.recentRequests > 100) {
      this.stats.recentRequests = 100;
      this.stats.recentErrorCount = Math.min(this.stats.recentErrorCount, 100);
    }
  }

  isHealthy() {
    const recentFailRate = this.stats.recentRequests > 0
      ? this.stats.recentErrorCount / this.stats.recentRequests
      : 0;

    const fiveMinAgo = Date.now() - 5 * 60 * 1000;
    const hasRecentSuccess = this.stats.lastSuccessTime && this.stats.lastSuccessTime > fiveMinAgo;

    // Stop conditions
    if (recentFailRate > 0.7 && !hasRecentSuccess) {
      return { healthy: false, reason: `High failure rate (${(recentFailRate*100).toFixed(1)}%) with no recent success` };
    }

    if (this.stats.recentErrors.length > 20 && this.stats.recentErrorCount > 10) {
      const errorTypes = new Set(this.stats.recentErrors.map(e => e.type)).size;
      if (errorTypes === 1) {
        return { healthy: false, reason: 'Repeated identical errors' };
      }
    }

    return { healthy: true };
  }

  /**
   * Check if a URL's domain is allowed by the whitelist
   * @param {string} urlStr - URL to check
   * @returns {boolean}
   */
  isAllowedDomain(urlStr) {
    try {
      const { hostname } = new URL(urlStr);
      // Strip leading www.
      const cleanHost = hostname.replace(/^www\./i, '');
      // Check against whitelist: exact match or subdomain match
      return this.config.domains.some(allowed => {
        return cleanHost === allowed || cleanHost.endsWith('.' + allowed);
      });
    } catch {
      return false;
    }
  }

  // ============================================================================
  // CHECKPOINT & AUTO-SAVE
  // ============================================================================

  maybeCheckpoint(force = false) {
    const now = Date.now();
    const timeSinceLast = now - this.lastCheckpoint;

    if (force || (this.stats.documentsIndexed % this.autoSaveAfter === 0) || timeSinceLast > this.checkpointInterval) {
      this.saveState();
      this.indexer.save(); // Persist index to disk
    }
  }

  // ============================================================================
  // COMPLETION DETECTION
  // ============================================================================

  isCompletion() {
    if (this.queue.length === 0) {
      return { complete: true, reason: 'Queue exhausted', success: true };
    }

    if (this.stats.documentsIndexed >= this.config.crawl.max_documents) {
      return { complete: true, reason: `Reached max_documents (${this.config.crawl.max_documents})`, success: true };
    }

    return { complete: false };
  }

  // ============================================================================
  // ROBOTS PARSER (CACHED) - with 24h TTL
  // ============================================================================

  async getRobotsParser(domain) {
    const now = Date.now();
    const cached = this.robotsCache.get(domain);
    if (cached) {
      const age = now - cached.fetchedAt;
      const ttl = 24 * 60 * 60 * 1000; // 24 hours
      if (age < ttl) {
        return cached.parser;
      } else {
        this.robotsCache.delete(domain); // expired
      }
    }

    const robotsUrl = `https://${domain}/robots.txt`;
    try {
      const resp = await fetch(robotsUrl, { timeout: 10000 });
      let parser;
      if (resp.ok) {
        const body = await resp.text();
        parser = RobotsParser(robotsUrl, body);
      } else {
        parser = RobotsParser(robotsUrl, '');
      }
      this.robotsCache.set(domain, { parser, fetchedAt: Date.now() });
      return parser;
    } catch (e) {
      this.log(`Failed to fetch robots.txt for ${domain}: ${e.message}`, 'warn');
      const parser = RobotsParser(robotsUrl, '');
      this.robotsCache.set(domain, { parser, fetchedAt: Date.now() });
      return parser;
    }
  }

  /**
   * Get crawl delay for a host from robots.txt (seconds)
   * Returns null if not specified
   * @param {string} hostname
   * @returns {Promise<number|null>}
   */
  async getCrawlDelay(hostname) {
    const parser = await this.getRobotsParser(hostname);
    return parser.getCrawlDelay(this.config.crawl.user_agent);
  }

  // ============================================================================
  // QUEUE MANAGEMENT
  // ============================================================================

  enqueue(rawUrl, depth = 0) {
    // Normalize URL first
    const normalized = urlNormalizer.normalizeUrl(rawUrl);
    if (!normalized) {
      return false;
    }

    // Check if already seen
    if (this.seen.has(normalized)) {
      return false;
    }

    // SSRF prevention: block private IP ranges
    try {
      const parsed = new URL(normalized);
      if (urlNormalizer.isBlockedHost(parsed.hostname)) {
        this.log(`Blocked private IP/host: ${parsed.hostname} (SSRF prevention)`, 'warn');
        return false;
      }
    } catch (e) {
      return false;
    }

    // Domain whitelist check
    if (!urlNormalizer.isWhitelisted(normalized, this.config.domains)) {
      this.stats.skipped_domain = (this.stats.skipped_domain || 0) + 1;
      return false;
    }

    // Depth check
    if (depth > this.config.crawl.depth) {
      return false;
    }

    this.seen.add(normalized);
    this.queue.push({ url: normalized, depth });
    this.stats.queued = (this.stats.queued || 0) + 1;
    return true;
  }

  // ============================================================================
  // FETCH WITH RETRY (Politeness - Phase 3.3)
  // ============================================================================

  async fetchWithRetry(url, options = {}, maxRetries = 3) {
    let attempt = 0;
    const isRetryable = (error, response) => {
      if (response && response.status >= 500) return true;
      if (error && ['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EAI_AGAIN'].includes(error.code)) {
        return true;
      }
      return false;
    };

    while (attempt <= maxRetries) {
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), (this.config.crawl.timeout || 30) * 1000);

        const resp = await fetch(url, {
          ...options,
          signal: controller.signal,
          headers: {
            'User-Agent': this.config.crawl.user_agent,
            ...options.headers
          }
        });
        clearTimeout(timeoutId);

        if (resp.status === 429) {
          const retryAfterHeader = resp.headers.get('Retry-After');
          let delay = 0;
          if (retryAfterHeader) {
            const seconds = parseInt(retryAfterHeader, 10);
            if (!isNaN(seconds)) {
              delay = seconds * 1000;
            } else {
              const date = new Date(retryAfterHeader);
              if (!isNaN(date.getTime())) {
                delay = date.getTime() - Date.now();
              }
            }
          }
          if (delay <= 0) delay = Math.pow(2, attempt) * 1000;

          if (attempt === maxRetries) {
            throw new Error(`Max retries (429) for ${url}`);
          }

          this.log(`429 Too Many Requests for ${url}, retry ${attempt + 1}/${maxRetries} after ${delay}ms`, 'warn');
          await new Promise(resolve => setTimeout(resolve, delay));
          attempt++;
          continue;
        }

        // Treat any other non-OK status as error
        if (!resp.ok) {
          const error = new Error(`HTTP error ${resp.status}`);
          error.status = resp.status;
          throw error;
        }

        return resp;
      } catch (error) {
        if (attempt === maxRetries || !isRetryable(error, null)) {
          throw error;
        }
        const delay = Math.pow(2, attempt) * 1000;
        this.log(`Fetch error ${url}: ${error.message}, retry ${attempt + 1}/${maxRetries} after ${delay}ms`, 'warn');
        await new Promise(resolve => setTimeout(resolve, delay));
        attempt++;
      }
    }
  }

  // ============================================================================
  // FETCH & PARSE
  // ============================================================================

  async fetchUrl(url, depth) {
    try {
      const resp = await this.fetchWithRetry(url);

      if (!resp.ok) {
        this.log(`HTTP ${resp.status} ${url}`, 'warn');
        return null;
      }

      const contentType = resp.headers.get('content-type') || '';
      if (!contentType.includes('text/html')) {
        this.log(`Skipping non-HTML: ${contentType} ${url}`, 'debug');
        return null;
      }

      const html = await resp.text();
      this.stats.requestsMade = (this.stats.requestsMade || 0) + 1;

      return this.parseHtml(url, html, depth);
    } catch (e) {
      this.stats.errors = (this.stats.errors || 0) + 1;
      this.log(`Fetch failed ${url}: ${e.message}`, 'warn');
      return null;
    }
  }

  /**
   * Parse HTML, extract title, content, excerpt, and links
   * Uses ContentExtractor for high-quality extraction
   */
  async parseHtml(url, html, depth) {
    try {
      // Use ContentExtractor for title and content
      const extracted = await this.extractor.extract(html, url);
      const hostname = new URL(url).hostname;

      // Extract links separately (still needed for crawl)
      const links = [];
      const { JSDOM } = require('jsdom');
      const dom = new JSDOM(html, { url });
      const doc = dom.window.document;
      const anchors = doc.querySelectorAll('a[href]');
      for (const a of anchors) {
        try {
          const href = a.getAttribute('href');
          if (!href || href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:')) continue;
          const absolute = new URL(href, url).href;
          links.push(absolute);
        } catch (e) {
          // ignore malformed
        }
      }

      return {
        url,
        title: extracted.title,
        content: extracted.content,
        excerpt: extracted.excerpt,
        quality: extracted.quality,
        issues: extracted.issues,
        type: extracted.type,
        domain: hostname,
        depth,
        links
      };
    } catch (e) {
      this.log(`Parse failed ${url}: ${e.message}`, 'error');
      return null;
    }
  }

  // ============================================================================
  // INDEXING
  // ============================================================================

  async indexDocument(doc) {
    try {
      this.indexer.addDocument(doc);
      this.stats.documentsIndexed = (this.stats.documentsIndexed || 0) + 1;
      this.stats.urlsExtracted = (this.stats.urlsExtracted || 0) + (doc.links?.length || 0);
      this.stats.domainBreakdown = this.stats.domainBreakdown || {};
      this.stats.domainBreakdown[doc.domain] = (this.stats.domainBreakdown[doc.domain] || 0) + 1;

      const maxDepth = this.stats.maxDepthReached || 0;
      if (doc.depth > maxDepth) {
        this.stats.maxDepthReached = doc.depth;
      }

      this.log(`Indexed: ${doc.title || doc.url} (${doc.domain})`, 'info');

      // Auto-save checkpoint
      this.maybeCheckpoint();
    } catch (e) {
      this.stats.errors = (this.stats.errors || 0) + 1;
      this.log(`Index failed ${doc.url}: ${e.message}`, 'error');
    }
  }

  // ============================================================================
  // MAIN CRAWL LOOP
  // ============================================================================

  async crawl() {
    this.running = true;
    this.startTime = Date.now();
    this.log(`Crawler started: ${this.queue.length} URLs queued, ${this.seen.size} seen`, 'info');

    while (this.running && this.queue.length > 0) {
      const { url, depth } = this.queue.shift();

      // Check completion conditions
      if (this.stats.documentsIndexed >= this.config.crawl.max_documents) {
        this.log(`Reached max_documents (${this.config.crawl.max_documents})`, 'info');
        break;
      }

      // Determine hostname for rate limiting
      let hostname = null;
      try {
        const parsedUrl = new URL(url);
        hostname = parsedUrl.hostname;
      } catch (e) {
        this.log(`Invalid URL, skipping: ${url}`, 'warn');
        this.updateHealthMetrics(false);
        continue;
      }

      // Politeness: per-host rate limiting
      await this.rateLimiter.waitForSlot(hostname);

      // Robots.txt check
      try {
        const robots = await this.getRobotsParser(hostname);
        if (this.config.crawl.respect_robots && !robots.isAllowed(url, this.config.crawl.user_agent)) {
          this.stats.robotsBlocked = (this.stats.robotsBlocked || 0) + 1;
          this.log(`Robots denied: ${url}`, 'debug');
          this.updateHealthMetrics(false);
          continue;
        }
      } catch (e) {
        this.log(`Robots check failed for ${hostname}: ${e.message}`, 'warn');
        // Continue anyway (permissive)
      }

      // Fetch and process
      const result = await this.fetchUrl(url, depth);
      if (result) {
        await this.indexDocument(result);

        // Enqueue links if within depth
        if (depth < this.config.crawl.depth) {
          for (const link of result.links) {
            this.enqueue(link, depth + 1);
          }
        }
        this.updateHealthMetrics(true);
      } else {
        this.updateHealthMetrics(false);
      }

      // Periodic checkpoint (time-based)
      this.maybeCheckpoint();

      // Health check
      const health = this.isHealthy();
      if (!health.healthy) {
        this.log(`Stopping due to health: ${health.reason}`, 'error');
        break;
      }
    }

    await this.finalize();
  }

  async finalize() {
    this.running = false;
    const completion = this.isCompletion();
    const duration = Date.now() - this.startTime;

    // Save index and state before exit
    this.indexer.save();
    this.saveState(); // Final checkpoint

    this.log(`Crawler ${completion.success ? 'completed' : 'stopped'}: ${completion.reason}`, 'info');
    this.log(`Duration: ${(duration/1000).toFixed(1)}s, Indexed: ${this.stats.documentsIndexed}, Fetched: ${this.stats.requestsMade}, Errors: ${this.stats.errors}`, 'info');

    // Simple stats printed
    console.log(`
=== Crawl Statistics ===
Documents indexed: ${this.stats.documentsIndexed}
Requests made: ${this.stats.requestsMade}
URLs extracted: ${this.stats.urlsExtracted}
Errors: ${this.stats.errors}
Robots blocked: ${this.stats.robotsBlocked || 0}
Skipped (domain): ${this.stats.skipped_domain || 0}
Max depth reached: ${this.stats.maxDepthReached || 0}
Domain breakdown:
${Object.entries(this.stats.domainBreakdown || {}).map(([d, c]) => `  ${d}: ${c}`).join('\n')}
========================
    `);

    // Exit cleanly
    process.exit(0);
  }

  // ============================================================================
  // PUBLIC API
  // ============================================================================

  stop() {
    if (this.running) {
      this.log('Stopping crawl...', 'info');
      this.saveState(); // Save before exit
    }
  }

  async start() {
    // If starting fresh, initialize seeds
    if (this.queue.length === 0) {
      this.log('Initializing crawl seeds', 'info');
      for (const seed of this.config.seeds) {
        this.enqueue(seed, 0);
      }
    }

    await this.crawl();
  }
}

module.exports = CuratedCrawler;


path: src/rate-limiter.js
content: /**
 * Rate Limiter - Phase 3.3
 *
 * Per-host rate limiting with configurable delay and robots.txt crawl-delay.
 * Ensures politeness by spacing requests to the same host.
 */

class RateLimiter {
  constructor(config, robotsManager) {
    this.config = config;
    this.robotsManager = robotsManager;
    this.buckets = new Map(); // hostname -> { lastRequest: timestamp }
  }

  /**
   * Wait until it's acceptable to make a request to the given host
   * @param {string} hostname - Hostname (no scheme, no path)
   */
  async waitForSlot(hostname) {
    const now = Date.now();
    const bucket = this.buckets.get(hostname) || { lastRequest: 0 };
    const delay = await this.getDelay(hostname);
    const timeSinceLast = now - bucket.lastRequest;

    if (timeSinceLast < delay) {
      const waitTime = delay - timeSinceLast;
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }

    bucket.lastRequest = Date.now();
    this.buckets.set(hostname, bucket);
  }

  /**
   * Compute effective delay for a host (ms)
   * Uses Math.max(config delay, robots crawl-delay) to be lenient (more conservative)
   * @param {string} hostname
   * @returns {Promise<number>} Delay in milliseconds
   */
  async getDelay(hostname) {
    const baseDelay = this.config.crawl.delay;
    // robotsManager.getCrawlDelay may be async if it needs to fetch
    const robotsDelay = await this.robotsManager.getCrawlDelay(hostname);
    if (robotsDelay) {
      // robots.txt delay is in seconds, convert to ms
      return Math.max(baseDelay, robotsDelay * 1000);
    }
    return baseDelay;
  }
}

module.exports = RateLimiter;


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)