Commit e0b7ced

hrbrmstr <bob@rud.is>
2025-11-21 13:41:22
chore: cli?
1 parent 0d8aca5
CLI.md
@@ -0,0 +1,457 @@
+# JA4 CLI Tool
+
+A TypeScript-based command-line interface for analyzing JA4+ network fingerprints with comprehensive threat intelligence integration.
+
+## Features
+
+- **Multi-format Support**: Analyze JA4, JA4S, JA4H, JA4X, and JA4T fingerprints
+- **Database Integration**: Automatic lookup against the JA4+ database
+- **Comparison Tools**: Compare fingerprints to identify similarities and differences
+- **Batch Processing**: Process multiple fingerprints from files
+- **Search Capabilities**: Search by application name or operating system
+- **Pretty Output**: Beautiful colored terminal output with progress indicators
+- **JSON Export**: Machine-readable output for automation
+
+## Installation
+
+### Prerequisites
+
+- Node.js >= 20.0.0
+- npm or yarn
+
+### Installation
+
+To install the CLI globally, run:
+
+```bash
+npm install -g
+```
+
+Alternatively, you can use the CLI directly without installation:
+
+```bash
+npx ja4-mcp-server cli [command]
+```
+
+### Install Dependencies
+
+```bash
+npm install
+# or
+yarn install
+```
+
+### Make CLI Executable
+
+```bash
+chmod +x cli.ts
+```
+
+## Usage
+
+### Basic Analysis
+
+Analyze a single JA4 fingerprint:
+
+```bash
+npm run cli analyze "t13d1516h2_8daaf6152771_b0da82dd1658"
+```
+
+Analyze different fingerprint types:
+
+```bash
+# JA4S (Server)
+npm run cli analyze -t ja4s "t13d15h2_002f_0035"
+
+# JA4H (HTTP)
+npm run cli analyze -t ja4h "ge11nn05_7a9c_b4a9_ad9e"
+
+# JA4T (TCP)
+npm run cli analyze -t ja4t "1460_020405b4_1460_8"
+```
+
+### Verbose Analysis
+
+Get detailed breakdown of all components:
+
+```bash
+npm run cli analyze -v "t13d1516h2_8daaf6152771_b0da82dd1658"
+```
+
+### Skip Database Lookup
+
+For faster analysis without database queries:
+
+```bash
+ja4 analyze --no-db "t13d1516h2_8daaf6152771_b0da82dd1658"
+```
+
+### JSON Output
+
+Get machine-readable output:
+
+```bash
+ja4 analyze -j "t13d1516h2_8daaf6152771_b0da82dd1658"
+```
+
+### Compare Fingerprints
+
+Compare two fingerprints to identify differences:
+
+```bash
+ja4 compare "t13d1516h2_8daaf6152771_b0da82dd1658" "t13d1517h2_8daaf6152771_7128f82b508a"
+```
+
+```bash
+ja4 compare "t13d1516h2_8daaf6152771_b0da82dd1658" "t13d1516h2_8daaf6152772_b0da82dd1659"
+```
+
+JSON comparison output:
+
+```bash
+ja4 compare -j "fp1" "fp2"
+```
+
+### Search Database
+
+Search by application name:
+
+```bash
+ja4 search "Chrome"
+ja4 search "Firefox"
+ja4 search -l 100 "Safari"  # Limit to 100 results
+```
+
+Search by operating system:
+
+```bash
+ja4 search -t os "Windows"
+ja4 search -t os "Linux"
+```
+
+### Database Statistics
+
+View database statistics:
+
+```bash
+ja4 stats
+```
+
+JSON output:
+
+```bash
+npm run cli stats -j
+```
+
+### Batch Processing
+
+Process multiple fingerprints from a file:
+
+Create a file `fingerprints.txt`:
+```
+t13d1516h2_8daaf6152771_b0da82dd1658
+t13d1715h2_9daaf6152771_c1da82dd1659
+t12d1314h1_5daaf6152771_a2da82dd1660
+```
+
+Process the batch:
+
+```bash
+ja4 batch fingerprints.txt
+```
+
+
+Process with options:
+
+```bash
+# Different fingerprint type
+ja4 batch -t ja4s server_fingerprints.txt
+
+# Save to JSON file
+ja4 batch -o results.json fingerprints.txt
+
+# Skip database lookups for speed
+ja4 batch --no-db fingerprints.txt
+```
+
+## Command Reference
+
+### `analyze`
+
+Analyze a single JA4+ fingerprint.
+
+**Usage:** `analyze <fingerprint> [options]`
+
+**Options:**
+- `-t, --type <type>` - Fingerprint type (ja4, ja4s, ja4h, ja4x, ja4t) [default: ja4]
+- `--no-db` - Skip database lookup
+- `-j, --json` - Output as JSON
+- `-v, --verbose` - Verbose output with detailed breakdown
+
+**Examples:**
+```bash
+npm run cli analyze "t13d1516h2_8daaf6152771_b0da82dd1658"
+npm run cli analyze -t ja4s -v "t13d15h2_002f_0035"
+npm run cli analyze -j --no-db "ge11nn05_7a9c_b4a9_ad9e"
+```
+
+### `compare`
+
+Compare two JA4+ fingerprints.
+
+**Usage:** `compare <fingerprint1> <fingerprint2> [options]`
+
+**Options:**
+- `-j, --json` - Output as JSON
+
+**Examples:**
+```bash
+npm run cli compare "fp1" "fp2"
+npm run cli compare -j "fp1" "fp2"
+```
+
+### `search`
+
+Search the JA4+ database.
+
+**Usage:** `search <query> [options]`
+
+**Options:**
+- `-t, --type <type>` - Search type (app or os) [default: app]
+- `-l, --limit <number>` - Limit results [default: 50]
+- `-j, --json` - Output as JSON
+
+**Examples:**
+```bash
+npm run cli search "Chrome"
+npm run cli search -t os "Windows"
+npm run cli search -l 100 -j "Firefox"
+```
+
+### `stats`
+
+Show database statistics.
+
+**Usage:** `stats [options]`
+
+**Options:**
+- `-j, --json` - Output as JSON
+
+**Examples:**
+```bash
+npm run cli stats
+npm run cli stats -j
+```
+
+### `batch`
+
+Process multiple fingerprints from a file.
+
+**Usage:** `batch <file> [options]`
+
+**Options:**
+- `-t, --type <type>` - Fingerprint type [default: ja4]
+- `-o, --output <file>` - Output file (JSON format)
+- `--no-db` - Skip database lookups
+
+**Examples:**
+```bash
+npm run cli batch fingerprints.txt
+npm run cli batch -t ja4s -o results.json server_fps.txt
+npm run cli batch --no-db large_dataset.txt
+```
+
+## Output Examples
+
+### Analysis Output (Pretty)
+
+```
+๐Ÿ“Š JA4 Analysis
+โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
+Fingerprint: t13d1516h2_8daaf6152771_b0da82dd1658
+Format: JA4
+Summary: TCP TLS 1.3 connection with SNI present, using HTTP/2
+
+๐Ÿ” Breakdown:
+  PART_A: t13d1516h2
+  PART_B: 8daaf6152771
+  PART_C: b0da82dd1658
+
+๐Ÿ’ก Use Cases:
+  โ€ข TLS client fingerprinting
+  โ€ข Bot detection
+  โ€ข Security analysis
+
+๐Ÿ—„๏ธ  Database Results (3 found):
+  1. Google Chrome
+     OS: Windows 10
+     โœ“ Verified
+  2. Chromium Browser
+     OS: Linux
+  3. Edge Browser
+     OS: Windows 11
+     โœ“ Verified
+```
+
+### Comparison Output (Pretty)
+
+```
+๐Ÿ” Fingerprint Comparison
+โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
+โš ๏ธ  Fingerprints differ
+
+Analysis: Fingerprints are similar (67% match) - likely same client type with minor differences
+
+โœ… Similarities:
+  Part A: t13d1516h2
+  Part B: 8daaf6152771
+
+โŒ Differences:
+  Part C:
+    FP1: b0da82dd1658
+    FP2: b0da82dd1659
+    Note: Different cryptographic hash values indicating different cipher/extension sets
+```
+
+### JSON Output
+
+```json
+{
+  "fingerprint": "t13d1516h2_8daaf6152771_b0da82dd1658",
+  "type": "JA4",
+  "analysis": {
+    "format": "JA4",
+    "breakdown": {
+      "part_a": {
+        "raw": "t13d1516h2",
+        "protocol": "TCP",
+        "tls_version": "TLS 1.3",
+        "sni_presence": "SNI Present",
+        "cipher_count": 21,
+        "extension_count": 22,
+        "alpn": "HTTP/2"
+      }
+    },
+    "human_readable": "TCP TLS 1.3 connection with SNI present, using HTTP/2"
+  },
+  "database": {
+    "count": 3,
+    "results": [
+      {
+        "ja4": "t13d1516h2_8daaf6152771_b0da82dd1658",
+        "application": "Google Chrome",
+        "os": "Windows 10",
+        "verified": true
+      }
+    ]
+  }
+}
+```
+
+## Development
+
+### Build the CLI
+
+```bash
+npm run build:cli
+```
+
+### Run in Development Mode
+
+```bash
+npm run cli -- analyze "fingerprint"
+```
+
+### Add New Commands
+
+1. Import the `Command` class from commander
+2. Add your command definition
+3. Implement the action handler
+4. Update this README
+
+### Error Handling
+
+The CLI includes comprehensive error handling:
+
+- Invalid fingerprint formats
+- Network connectivity issues
+- File I/O problems
+- Malformed data
+
+All errors are displayed with colored output and appropriate exit codes.
+
+## Integration
+
+### Use with Scripts
+
+```bash
+# Check exit code
+npm run cli analyze "fingerprint" && echo "Success" || echo "Failed"
+
+# Pipe JSON output
+npm run cli analyze -j "fingerprint" | jq '.analysis.human_readable'
+
+# Batch processing with custom logic
+while read -r fp; do
+  npm run cli analyze -j "$fp" >> results.jsonl
+done < fingerprints.txt
+```
+
+### CI/CD Integration
+
+```yaml
+# GitHub Actions example
+- name: Analyze fingerprints
+  run: |
+    npm install
+    npm run cli batch --no-db -o analysis.json fingerprints.txt
+    
+- name: Upload results
+  uses: actions/upload-artifact@v2
+  with:
+    name: ja4-analysis
+    path: analysis.json
+```
+
+## Performance Tips
+
+1. Use `--no-db` flag for faster analysis when database lookup isn't needed
+2. Process large batches in chunks to avoid memory issues
+3. Use JSON output for programmatic processing
+4. Cache database locally (happens automatically)
+
+## Troubleshooting
+
+### Database Download Issues
+
+If database download fails:
+
+```bash
+# Check network connectivity
+curl -I https://ja4db.com/api/download/ja4plus_db.json
+
+# Clear cache and retry
+rm -rf ~/.cache/ja4-cli
+npm run cli stats
+```
+
+### TypeScript Errors
+
+Ensure you have the correct Node.js version:
+
+```bash
+node --version  # Should be >= 20.0.0
+npm install     # Reinstall dependencies
+```
+
+### Performance Issues
+
+For large datasets:
+
+```bash
+# Use smaller batch sizes
+split -l 1000 large_dataset.txt batch_
+for file in batch_*; do
+  npm run cli batch "$file" -o "results_$file.json"
+done
+```
cli.ts
@@ -0,0 +1,1016 @@
+#!/usr/bin/env tsx
+import { Command } from "commander";
+import chalk from "chalk";
+import ora from "ora";
+import { readFileSync, existsSync, createWriteStream, statSync } from "fs";
+import { promises as fs } from "fs";
+import path from "path";
+import os from "os";
+import https from "https";
+
+// Types
+interface JA4Analysis {
+  format: string;
+  breakdown: {
+    part_a: {
+      raw: string;
+      protocol?: string;
+      tls_version?: string;
+      sni_presence?: string;
+      cipher_count?: number;
+      extension_count?: number;
+      alpn?: string;
+      method?: string;
+      version?: string;
+      has_cookies?: boolean;
+      has_referer?: boolean;
+      header_count?: number;
+      language?: string;
+      description?: string;
+      note?: string;
+      value?: string | number;
+    };
+    part_b: {
+      raw: string;
+      description: string;
+      note: string;
+    };
+    part_c: {
+      raw: string;
+      description: string;
+      note: string;
+    };
+    part_d?: {
+      raw: string;
+      description: string;
+      note: string;
+    };
+  };
+  human_readable?: string;
+  use_cases?: string[];
+  analysis?: {
+    actual_window_size?: number;
+    network_overhead?: string;
+    os_indicators?: string[];
+    tunnel_vpn_indicators?: string[];
+  };
+}
+
+interface DatabaseResult {
+  ja4?: string;
+  ja4s?: string;
+  ja4h?: string;
+  ja4x?: string;
+  ja4t?: string;
+  application?: string;
+  os?: string;
+  verified?: boolean;
+  observation_count?: number;
+}
+
+interface SearchResult {
+  count: number;
+  results: DatabaseResult[];
+}
+
+interface ComparisonResult {
+  fingerprint_1: string;
+  fingerprint_2: string;
+  identical: boolean;
+  similarities: Array<{ part: string; value: string; note: string }>;
+  differences: Array<{ part: string; fp1: string; fp2: string; note: string }>;
+  analysis: string;
+}
+
+interface DatabaseStats {
+  status: string;
+  total_records: number;
+  ja4_count: number;
+  ja4s_count: number;
+  ja4h_count: number;
+  ja4x_count: number;
+  ja4t_count: number;
+  verified_count: number;
+  applications: number;
+  operating_systems: number;
+  last_update: string;
+}
+
+// Database URL
+const DB_URL = "https://ja4db.com/api/download/";
+
+// Get XDG cache directory
+function getXDGCacheDir(): string {
+  if (process.platform === "win32") {
+    return process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
+  }
+
+  const homeDir = os.homedir();
+  return process.env.XDG_CACHE_HOME || path.join(homeDir, ".cache");
+}
+
+const CACHE_DIR = path.join(getXDGCacheDir(), "ja4-cli");
+const DB_FILE = path.join(CACHE_DIR, "ja4plus_db.json");
+const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
+
+class JA4Database {
+  private database: any = null;
+
+  async ensureCacheDir(): Promise<void> {
+    try {
+      await fs.mkdir(CACHE_DIR, { recursive: true });
+    } catch (error) {
+      // Directory might already exist
+    }
+  }
+
+  async downloadDatabase(): Promise<void> {
+    await this.ensureCacheDir();
+
+    return new Promise((resolve, reject) => {
+      const file = createWriteStream(DB_FILE);
+
+      https
+        .get(DB_URL, (response) => {
+          if (response.statusCode !== 200) {
+            reject(new Error(`Failed to download database: ${response.statusCode}`));
+            return;
+          }
+
+          response.pipe(file);
+          file.on("finish", () => {
+            file.close();
+            resolve();
+          });
+        })
+        .on("error", reject);
+    });
+  }
+
+  async loadDatabase(): Promise<void> {
+    try {
+      const stats = await fs.stat(DB_FILE);
+      const age = Date.now() - stats.mtime.getTime();
+
+      if (age > CACHE_DURATION) {
+        await this.downloadDatabase();
+      }
+
+      const content = await fs.readFile(DB_FILE, "utf8");
+      this.database = JSON.parse(content);
+    } catch (error) {
+      await this.downloadDatabase();
+      const content = await fs.readFile(DB_FILE, "utf8");
+      this.database = JSON.parse(content);
+    }
+  }
+
+  lookupJA4(fingerprint: string): DatabaseResult[] {
+    if (!this.database) return [];
+    return this.database.filter((entry: any) => entry.ja4 === fingerprint);
+  }
+
+  lookupJA4S(fingerprint: string): DatabaseResult[] {
+    if (!this.database) return [];
+    return this.database.filter((entry: any) => entry.ja4s === fingerprint);
+  }
+
+  lookupJA4H(fingerprint: string): DatabaseResult[] {
+    if (!this.database) return [];
+    return this.database.filter((entry: any) => entry.ja4h === fingerprint);
+  }
+
+  lookupJA4X(fingerprint: string): DatabaseResult[] {
+    if (!this.database) return [];
+    return this.database.filter((entry: any) => entry.ja4x === fingerprint);
+  }
+
+  lookupJA4T(fingerprint: string): DatabaseResult[] {
+    if (!this.database) return [];
+    return this.database.filter((entry: any) => entry.ja4t === fingerprint);
+  }
+
+  searchByApplication(query: string, limit: number = 50): SearchResult {
+    if (!this.database) return { count: 0, results: [] };
+
+    const searchTerm = query.toLowerCase();
+    const results = this.database
+      .filter((entry: any) => entry.application && entry.application.toLowerCase().includes(searchTerm))
+      .slice(0, limit);
+
+    return {
+      count: results.length,
+      results,
+    };
+  }
+
+  searchByOS(query: string, limit: number = 50): SearchResult {
+    if (!this.database) return { count: 0, results: [] };
+
+    const searchTerm = query.toLowerCase();
+    const results = this.database
+      .filter((entry: any) => entry.os && entry.os.toLowerCase().includes(searchTerm))
+      .slice(0, limit);
+
+    return {
+      count: results.length,
+      results,
+    };
+  }
+
+  getStatistics(): DatabaseStats {
+    if (!this.database) {
+      return {
+        status: "No database loaded",
+        total_records: 0,
+        ja4_count: 0,
+        ja4s_count: 0,
+        ja4h_count: 0,
+        ja4x_count: 0,
+        ja4t_count: 0,
+        verified_count: 0,
+        applications: 0,
+        operating_systems: 0,
+        last_update: "Unknown",
+      };
+    }
+
+    const total_records = this.database.length;
+    const ja4_count = this.database.filter((entry: any) => entry.ja4).length;
+    const ja4s_count = this.database.filter((entry: any) => entry.ja4s).length;
+    const ja4h_count = this.database.filter((entry: any) => entry.ja4h).length;
+    const ja4x_count = this.database.filter((entry: any) => entry.ja4x).length;
+    const ja4t_count = this.database.filter((entry: any) => entry.ja4t).length;
+    const verified_count = this.database.filter((entry: any) => entry.verified).length;
+    const applications = new Set(this.database.map((entry: any) => entry.application).filter(Boolean)).size;
+    const operating_systems = new Set(this.database.map((entry: any) => entry.os).filter(Boolean)).size;
+
+    let last_update = "Unknown";
+    try {
+      const stats = statSync(DB_FILE);
+      last_update = stats.mtime.toISOString();
+    } catch (error) {
+      // Ignore error
+    }
+
+    return {
+      status: "Database loaded successfully",
+      total_records,
+      ja4_count,
+      ja4s_count,
+      ja4h_count,
+      ja4x_count,
+      ja4t_count,
+      verified_count,
+      applications,
+      operating_systems,
+      last_update,
+    };
+  }
+}
+
+class JA4Analyzer {
+  static parseJA4(fingerprint: string): JA4Analysis {
+    const parts = fingerprint.split("_");
+    if (parts.length !== 3) {
+      throw new Error("Invalid JA4 fingerprint format");
+    }
+
+    const [partA, partB, partC] = parts;
+
+    const protocol = partA.charAt(0);
+    const version = partA.substring(1, 3);
+    const sni = partA.charAt(3);
+    const cipherCount = parseInt(partA.substring(4, 6));
+    const extensionCount = parseInt(partA.substring(6, 8));
+    const alpn = partA.substring(8);
+
+    return {
+      format: "JA4",
+      breakdown: {
+        part_a: {
+          raw: partA,
+          protocol: this.explainProtocol(protocol).description,
+          tls_version: this.explainTLSVersion(version).description,
+          sni_presence: this.explainSNI(sni).description,
+          cipher_count: cipherCount,
+          extension_count: extensionCount,
+          alpn: this.explainALPN(alpn).description,
+        },
+        part_b: {
+          raw: partB,
+          description: "SHA256 hash of cipher suites in order",
+          note: "First 12 characters of hash",
+        },
+        part_c: {
+          raw: partC,
+          description: "SHA256 hash of extensions in order",
+          note: "First 12 characters of hash",
+        },
+      },
+      human_readable: this.generateHumanReadable(protocol, version, sni, alpn),
+    };
+  }
+
+  static parseJA4S(fingerprint: string): JA4Analysis {
+    const parts = fingerprint.split("_");
+    if (parts.length !== 3) {
+      throw new Error("Invalid JA4S fingerprint format");
+    }
+
+    const [partA, partB, partC] = parts;
+
+    const protocol = partA.charAt(0);
+    const version = partA.substring(1, 3);
+    const cipherCount = parseInt(partA.substring(3, 5));
+    const extensionCount = parseInt(partA.substring(5, 7));
+    const alpn = partA.substring(7);
+
+    return {
+      format: "JA4S",
+      breakdown: {
+        part_a: {
+          raw: partA,
+          protocol: this.explainProtocol(protocol).description,
+          tls_version: this.explainTLSVersion(version).description,
+          cipher_count: cipherCount,
+          extension_count: extensionCount,
+          alpn: this.explainALPN(alpn).description,
+        },
+        part_b: {
+          raw: partB,
+          description: "SHA256 hash of cipher suite",
+          note: "First 12 characters of hash",
+        },
+        part_c: {
+          raw: partC,
+          description: "SHA256 hash of extensions",
+          note: "First 12 characters of hash",
+        },
+      },
+      human_readable: `JA4S Server: ${this.explainProtocol(protocol).description} ${this.explainTLSVersion(version).description}`,
+    };
+  }
+
+  static parseJA4H(fingerprint: string): JA4Analysis {
+    const parts = fingerprint.split("_");
+    if (parts.length !== 4) {
+      throw new Error("Invalid JA4H fingerprint format");
+    }
+
+    const [partA, partB, partC, partD] = parts;
+
+    const method = partA.substring(0, 2);
+    const version = partA.substring(2, 4);
+    const cookiePresence = partA.charAt(4);
+    const refererPresence = partA.charAt(5);
+    const headerCount = parseInt(partA.substring(6, 8));
+    const languageHeader = partA.substring(8);
+
+    return {
+      format: "JA4H",
+      breakdown: {
+        part_a: {
+          raw: partA,
+          method: this.explainHTTPMethod(method).method,
+          version: this.explainHTTPVersion(version).description,
+          has_cookies: cookiePresence === "c",
+          has_referer: refererPresence === "r",
+          header_count: headerCount,
+          language: languageHeader,
+        },
+        part_b: {
+          raw: partB,
+          description: "SHA256 hash of header names",
+          note: "First 12 characters of hash",
+        },
+        part_c: {
+          raw: partC,
+          description: "SHA256 hash of header values",
+          note: "First 12 characters of hash",
+        },
+        part_d: {
+          raw: partD,
+          description: "SHA256 hash of cookies",
+          note: "First 12 characters of hash",
+        },
+      },
+      use_cases: ["HTTP client fingerprinting", "Bot detection", "User-Agent validation", "Header analysis"],
+    };
+  }
+
+  static parseJA4X(fingerprint: string): JA4Analysis {
+    const parts = fingerprint.split("_");
+    if (parts.length !== 3) {
+      throw new Error("Invalid JA4X fingerprint format");
+    }
+
+    return {
+      format: "JA4X",
+      breakdown: {
+        part_a: {
+          raw: parts[0],
+          description: "X.509 certificate details",
+          note: "Certificate chain information",
+        },
+        part_b: {
+          raw: parts[1],
+          description: "Certificate algorithms",
+          note: "Signature and key algorithms",
+        },
+        part_c: {
+          raw: parts[2],
+          description: "Certificate extensions",
+          note: "Extension fields and values",
+        },
+      },
+      use_cases: ["Certificate fingerprinting", "TLS server identification", "Certificate chain analysis"],
+    };
+  }
+
+  static parseJA4T(fingerprint: string): JA4Analysis {
+    const parts = fingerprint.split("_");
+    if (parts.length !== 4) {
+      throw new Error("Invalid JA4T fingerprint format");
+    }
+
+    const [windowSize, tcpOptions, mss, windowScale] = parts;
+
+    return {
+      format: "JA4T",
+      breakdown: {
+        part_a: {
+          raw: windowSize,
+          description: "TCP window size",
+          note: "Initial window size from SYN packet",
+        },
+        part_b: {
+          raw: tcpOptions,
+          description: "TCP options",
+          note: "Options from TCP header",
+        },
+        part_c: {
+          raw: mss,
+          description: "Maximum Segment Size",
+          note: "MSS value from TCP options",
+        },
+        part_d: {
+          raw: windowScale,
+          description: "Window scale factor",
+          note: "Window scaling option value",
+        },
+      },
+      analysis: {
+        actual_window_size: parseInt(windowSize) * Math.pow(2, parseInt(windowScale) || 0),
+        network_overhead: this.analyzeNetworkOverhead(parseInt(mss)),
+        os_indicators: this.analyzeTCPOSIndicators(tcpOptions),
+        tunnel_vpn_indicators: this.analyzeTunnelIndicators(parseInt(mss)),
+      },
+      use_cases: [
+        "Operating system fingerprinting",
+        "Network stack identification",
+        "VPN/Tunnel detection",
+        "Network performance analysis",
+      ],
+    };
+  }
+
+  static compareFingerprints(fp1: string, fp2: string): ComparisonResult {
+    const parts1 = fp1.split("_");
+    const parts2 = fp2.split("_");
+
+    if (parts1.length !== parts2.length) {
+      throw new Error("Fingerprints must be of the same type");
+    }
+
+    const similarities: Array<{ part: string; value: string; note: string }> = [];
+    const differences: Array<{ part: string; fp1: string; fp2: string; note: string }> = [];
+
+    const partNames = ["A", "B", "C", "D"];
+
+    for (let i = 0; i < parts1.length; i++) {
+      const letter = partNames[i];
+
+      if (parts1[i] === parts2[i]) {
+        similarities.push({
+          part: `Part ${letter}`,
+          value: parts1[i],
+          note: "Identical values",
+        });
+      } else {
+        differences.push({
+          part: `Part ${letter}`,
+          fp1: parts1[i],
+          fp2: parts2[i],
+          note: this.getDifferenceNote(letter, parts1[i], parts2[i]),
+        });
+      }
+    }
+
+    const identical = differences.length === 0;
+    const analysis = this.generateComparisonAnalysis(similarities, differences);
+
+    return {
+      fingerprint_1: fp1,
+      fingerprint_2: fp2,
+      identical,
+      similarities,
+      differences,
+      analysis,
+    };
+  }
+
+  private static explainProtocol(code: string): { description: string; full_name: string } {
+    const protocols: Record<string, { description: string; full_name: string }> = {
+      t: { description: "TCP", full_name: "Transmission Control Protocol" },
+      q: { description: "QUIC", full_name: "Quick UDP Internet Connections" },
+      d: { description: "DTLS", full_name: "Datagram Transport Layer Security" },
+    };
+
+    return protocols[code] || { description: "Unknown", full_name: "Unknown Protocol" };
+  }
+
+  private static explainTLSVersion(version: string): { description: string; security: string } {
+    const versions: Record<string, { description: string; security: string }> = {
+      "10": { description: "TLS 1.0", security: "Deprecated - Not Secure" },
+      "11": { description: "TLS 1.1", security: "Deprecated - Not Secure" },
+      "12": { description: "TLS 1.2", security: "Secure" },
+      "13": { description: "TLS 1.3", security: "Most Secure" },
+    };
+
+    return versions[version] || { description: "Unknown", security: "Unknown" };
+  }
+
+  private static explainSNI(code: string): { description: string; note: string } {
+    const sni: Record<string, { description: string; note: string }> = {
+      d: { description: "SNI Present", note: "Server Name Indication extension present" },
+      i: {
+        description: "SNI Not Present",
+        note: "Server Name Indication extension not present or IP address used",
+      },
+    };
+
+    return sni[code] || { description: "Unknown", note: "Unknown SNI status" };
+  }
+
+  private static explainALPN(code: string): { description: string; protocol: string } {
+    const alpns: Record<string, { description: string; protocol: string }> = {
+      h1: { description: "HTTP/1.1", protocol: "HTTP/1.1" },
+      h2: { description: "HTTP/2", protocol: "HTTP/2" },
+      h3: { description: "HTTP/3", protocol: "HTTP/3" },
+      dt: { description: "DoT", protocol: "DNS over TLS" },
+      dq: { description: "DoQ", protocol: "DNS over QUIC" },
+    };
+
+    return alpns[code] || { description: "None or Other", protocol: "Unknown" };
+  }
+
+  private static explainHTTPMethod(code: string): { method: string; description: string } {
+    const methods: Record<string, { method: string; description: string }> = {
+      ge: { method: "GET", description: "Retrieve data from server" },
+      po: { method: "POST", description: "Send data to server" },
+      he: { method: "HEAD", description: "Get headers only" },
+      pu: { method: "PUT", description: "Update/replace resource" },
+      de: { method: "DELETE", description: "Remove resource" },
+      op: { method: "OPTIONS", description: "Get allowed methods" },
+      pa: { method: "PATCH", description: "Partial update" },
+    };
+
+    return methods[code] || { method: "Unknown", description: "Unknown HTTP method" };
+  }
+
+  private static explainHTTPVersion(version: string): { description: string } {
+    const versions: Record<string, { description: string }> = {
+      "10": { description: "HTTP/1.0" },
+      "11": { description: "HTTP/1.1" },
+      "20": { description: "HTTP/2" },
+      "30": { description: "HTTP/3" },
+    };
+
+    return versions[version] || { description: "Unknown HTTP version" };
+  }
+
+  private static generateHumanReadable(protocol: string, version: string, sni: string, alpn: string): string {
+    const proto = this.explainProtocol(protocol);
+    const ver = this.explainTLSVersion(version);
+    const sniInfo = this.explainSNI(sni);
+    const alpnInfo = this.explainALPN(alpn);
+
+    return `${proto.description} ${ver.description} connection with ${sniInfo.description.toLowerCase()}, using ${alpnInfo.description}`;
+  }
+
+  private static getDifferenceNote(part: string, value1: string, value2: string): string {
+    if (part === "A") {
+      return "Different TLS configuration parameters";
+    } else {
+      return "Different cryptographic hash values indicating different cipher/extension sets";
+    }
+  }
+
+  private static generateComparisonAnalysis(similarities: any[], differences: any[]): string {
+    const simCount = similarities.length;
+    const diffCount = differences.length;
+    const total = simCount + diffCount;
+
+    if (diffCount === 0) {
+      return "Fingerprints are identical - same client configuration";
+    } else if (simCount > diffCount) {
+      return `Fingerprints are similar (${Math.round((simCount / total) * 100)}% match) - likely same client type with minor differences`;
+    } else {
+      return `Fingerprints are different (${Math.round((simCount / total) * 100)}% match) - likely different client types or major configuration differences`;
+    }
+  }
+
+  private static analyzeNetworkOverhead(mss: number): string {
+    if (mss <= 536) return "High overhead (dialup/satellite)";
+    if (mss <= 1460) return "Standard Ethernet";
+    if (mss <= 8960) return "Jumbo frames";
+    return "Custom MTU";
+  }
+
+  private static analyzeTCPOSIndicators(options: string): string[] {
+    // Simplified OS detection based on TCP options
+    const indicators = [];
+    if (options.includes("020405b4")) indicators.push("Linux");
+    if (options.includes("020405dc")) indicators.push("Windows");
+    if (options.includes("02040578")) indicators.push("macOS");
+    return indicators.length ? indicators : ["Unknown"];
+  }
+
+  private static analyzeTunnelIndicators(mss: number): string[] {
+    const indicators = [];
+    if (mss < 1460) indicators.push("Possible VPN/Tunnel");
+    if (mss === 1436) indicators.push("Common VPN MSS");
+    return indicators;
+  }
+}
+
+// CLI Implementation
+const program = new Command();
+const db = new JA4Database();
+
+program.name("ja4-cli").description("JA4 fingerprint analysis CLI tool").version("1.0.0");
+
+program
+  .command("analyze")
+  .description("Analyze a JA4 fingerprint")
+  .argument("<fingerprint>", "Fingerprint to analyze")
+  .option("-t, --type <type>", "Fingerprint type", "ja4")
+  .option("--no-db", "Skip database lookup")
+  .option("-j, --json", "Output as JSON")
+  .option("-v, --verbose", "Verbose output")
+  .action(async (fingerprint: string, options) => {
+    const spinner = ora("Analyzing fingerprint...").start();
+
+    try {
+      let analysis: JA4Analysis;
+      let dbResults: DatabaseResult[] = [];
+
+      // Load database if needed
+      if (options.db) {
+        spinner.text = "Loading database...";
+        await db.loadDatabase();
+      }
+
+      // Analyze fingerprint
+      spinner.text = "Parsing fingerprint...";
+      const type = options.type.toLowerCase();
+
+      switch (type) {
+        case "ja4":
+          analysis = JA4Analyzer.parseJA4(fingerprint);
+          if (options.db) dbResults = db.lookupJA4(fingerprint);
+          break;
+        case "ja4s":
+          analysis = JA4Analyzer.parseJA4S(fingerprint);
+          if (options.db) dbResults = db.lookupJA4S(fingerprint);
+          break;
+        case "ja4h":
+          analysis = JA4Analyzer.parseJA4H(fingerprint);
+          if (options.db) dbResults = db.lookupJA4H(fingerprint);
+          break;
+        case "ja4x":
+          analysis = JA4Analyzer.parseJA4X(fingerprint);
+          if (options.db) dbResults = db.lookupJA4X(fingerprint);
+          break;
+        case "ja4t":
+          analysis = JA4Analyzer.parseJA4T(fingerprint);
+          if (options.db) dbResults = db.lookupJA4T(fingerprint);
+          break;
+        default:
+          throw new Error(`Unknown fingerprint type: ${type}`);
+      }
+
+      spinner.succeed("Analysis complete!");
+
+      if (options.json) {
+        console.log(
+          JSON.stringify(
+            {
+              fingerprint,
+              type: type.toUpperCase(),
+              analysis,
+              database: {
+                count: dbResults.length,
+                results: dbResults,
+              },
+            },
+            null,
+            2,
+          ),
+        );
+      } else {
+        // Pretty output
+        console.log(chalk.cyan(`\n๐Ÿ“Š ${type.toUpperCase()} Analysis`));
+        console.log(chalk.gray("โ”€".repeat(50)));
+        console.log(chalk.yellow("Fingerprint:"), fingerprint);
+        console.log(chalk.yellow("Format:"), analysis.format);
+
+        if (analysis.human_readable) {
+          console.log(chalk.yellow("Summary:"), analysis.human_readable);
+        }
+
+        console.log(chalk.cyan("\n๐Ÿ” Breakdown:"));
+        Object.entries(analysis.breakdown).forEach(([part, data]) => {
+          console.log(chalk.magenta(`  ${part.toUpperCase()}:`), data.raw);
+          if (options.verbose) {
+            Object.entries(data).forEach(([key, value]) => {
+              if (key !== "raw" && value !== undefined) {
+                console.log(chalk.gray(`    ${key}:`), value);
+              }
+            });
+          }
+        });
+
+        if (analysis.use_cases) {
+          console.log(chalk.cyan("\n๐Ÿ’ก Use Cases:"));
+          analysis.use_cases.forEach((useCase) => {
+            console.log(chalk.green(`  โ€ข ${useCase}`));
+          });
+        }
+
+        if (dbResults.length > 0) {
+          console.log(chalk.cyan(`\n๐Ÿ—„๏ธ  Database Results (${dbResults.length} found):`));
+          dbResults.slice(0, 5).forEach((result, i) => {
+            console.log(chalk.green(`  ${i + 1}.`), result.application || "Unknown Application");
+            if (result.os) console.log(chalk.gray(`     OS: ${result.os}`));
+            if (result.verified) console.log(chalk.blue("     โœ“ Verified"));
+          });
+          if (dbResults.length > 5) {
+            console.log(chalk.gray(`  ... and ${dbResults.length - 5} more`));
+          }
+        } else if (options.db) {
+          console.log(chalk.yellow("\n๐Ÿ—„๏ธ  No database matches found"));
+        }
+      }
+    } catch (error) {
+      spinner.fail("Analysis failed");
+      console.error(chalk.red("Error:"), (error as Error).message);
+      process.exit(1);
+    }
+  });
+
+program
+  .command("compare")
+  .description("Compare two JA4 fingerprints")
+  .argument("<fingerprint1>", "First fingerprint")
+  .argument("<fingerprint2>", "Second fingerprint")
+  .option("-j, --json", "Output as JSON")
+  .action((fp1: string, fp2: string, options) => {
+    try {
+      const comparison = JA4Analyzer.compareFingerprints(fp1, fp2);
+
+      if (options.json) {
+        console.log(JSON.stringify(comparison, null, 2));
+      } else {
+        console.log(chalk.cyan("\n๐Ÿ” Fingerprint Comparison"));
+        console.log(chalk.gray("โ”€".repeat(50)));
+
+        if (comparison.identical) {
+          console.log(chalk.green("โœ… Fingerprints are identical!"));
+        } else {
+          console.log(chalk.yellow("โš ๏ธ  Fingerprints differ"));
+        }
+
+        console.log(chalk.yellow("\nAnalysis:"), comparison.analysis);
+
+        if (comparison.similarities.length > 0) {
+          console.log(chalk.cyan("\nโœ… Similarities:"));
+          comparison.similarities.forEach((sim) => {
+            console.log(chalk.green(`  ${sim.part}: ${sim.value}`));
+          });
+        }
+
+        if (comparison.differences.length > 0) {
+          console.log(chalk.cyan("\nโŒ Differences:"));
+          comparison.differences.forEach((diff) => {
+            console.log(chalk.red(`  ${diff.part}:`));
+            console.log(chalk.gray(`    FP1: ${diff.fp1}`));
+            console.log(chalk.gray(`    FP2: ${diff.fp2}`));
+            console.log(chalk.gray(`    Note: ${diff.note}`));
+          });
+        }
+      }
+    } catch (error) {
+      console.error(chalk.red("Error:"), (error as Error).message);
+      process.exit(1);
+    }
+  });
+
+program
+  .command("search")
+  .description("Search the JA4 database")
+  .argument("<query>", "Search term")
+  .option("-t, --type <type>", "Search type (app or os)", "app")
+  .option("-l, --limit <number>", "Limit results", "50")
+  .option("-j, --json", "Output as JSON")
+  .action(async (query: string, options) => {
+    const spinner = ora("Searching database...").start();
+
+    try {
+      await db.loadDatabase();
+
+      let results: SearchResult;
+      const limit = parseInt(options.limit);
+
+      if (options.type === "os") {
+        results = db.searchByOS(query, limit);
+      } else {
+        results = db.searchByApplication(query, limit);
+      }
+
+      spinner.succeed(`Found ${results.count} results`);
+
+      if (options.json) {
+        console.log(JSON.stringify(results, null, 2));
+      } else {
+        console.log(chalk.cyan(`\n๐Ÿ” Search Results for "${query}"`));
+        console.log(chalk.gray("โ”€".repeat(50)));
+        console.log(chalk.yellow(`Found ${results.count} matches\n`));
+
+        results.results.forEach((result, i) => {
+          console.log(chalk.green(`${i + 1}.`), result.application || "Unknown Application");
+          if (result.os) console.log(chalk.gray(`   OS: ${result.os}`));
+          if (result.ja4) console.log(chalk.blue(`   JA4: ${result.ja4}`));
+          if (result.verified) console.log(chalk.magenta("   โœ“ Verified"));
+          console.log();
+        });
+      }
+    } catch (error) {
+      spinner.fail("Search failed");
+      console.error(chalk.red("Error:"), (error as Error).message);
+      process.exit(1);
+    }
+  });
+
+program
+  .command("stats")
+  .description("Show database statistics")
+  .option("-j, --json", "Output as JSON")
+  .action(async (options) => {
+    const spinner = ora("Loading database statistics...").start();
+
+    try {
+      await db.loadDatabase();
+      const stats = db.getStatistics();
+
+      spinner.succeed("Statistics loaded");
+
+      if (options.json) {
+        console.log(JSON.stringify(stats, null, 2));
+      } else {
+        console.log(chalk.cyan("\n๐Ÿ“Š Database Statistics"));
+        console.log(chalk.gray("โ”€".repeat(50)));
+        console.log(chalk.yellow("Status:"), stats.status);
+        console.log(chalk.yellow("Total Records:"), stats.total_records.toLocaleString());
+        console.log(chalk.yellow("Last Update:"), new Date(stats.last_update).toLocaleString());
+
+        console.log(chalk.cyan("\n๐Ÿ”ข Fingerprint Counts:"));
+        console.log(chalk.green("  JA4:"), stats.ja4_count.toLocaleString());
+        console.log(chalk.green("  JA4S:"), stats.ja4s_count.toLocaleString());
+        console.log(chalk.green("  JA4H:"), stats.ja4h_count.toLocaleString());
+        console.log(chalk.green("  JA4X:"), stats.ja4x_count.toLocaleString());
+        console.log(chalk.green("  JA4T:"), stats.ja4t_count.toLocaleString());
+
+        console.log(chalk.cyan("\nโœ… Quality Metrics:"));
+        console.log(chalk.blue("  Verified Records:"), stats.verified_count.toLocaleString());
+        console.log(chalk.blue("  Unique Applications:"), stats.applications.toLocaleString());
+        console.log(chalk.blue("  Unique Operating Systems:"), stats.operating_systems.toLocaleString());
+      }
+    } catch (error) {
+      spinner.fail("Failed to load statistics");
+      console.error(chalk.red("Error:"), (error as Error).message);
+      process.exit(1);
+    }
+  });
+
+program
+  .command("batch")
+  .description("Analyze multiple fingerprints from a file")
+  .argument("<file>", "File containing fingerprints (one per line)")
+  .option("-t, --type <type>", "Fingerprint type", "ja4")
+  .option("-o, --output <file>", "Output file (JSON format)")
+  .option("--no-db", "Skip database lookups")
+  .action(async (inputFile: string, options) => {
+    const spinner = ora("Processing batch file...").start();
+
+    try {
+      if (!existsSync(inputFile)) {
+        throw new Error(`Input file not found: ${inputFile}`);
+      }
+
+      const content = readFileSync(inputFile, "utf8");
+      const fingerprints = content.split("\n").filter((line) => line.trim());
+
+      if (options.db) {
+        spinner.text = "Loading database...";
+        await db.loadDatabase();
+      }
+
+      const results = [];
+      let processed = 0;
+
+      for (const fingerprint of fingerprints) {
+        try {
+          processed++;
+          spinner.text = `Processing ${processed}/${fingerprints.length}...`;
+
+          let analysis: JA4Analysis;
+          let dbResults: DatabaseResult[] = [];
+
+          switch (options.type.toLowerCase()) {
+            case "ja4":
+              analysis = JA4Analyzer.parseJA4(fingerprint.trim());
+              if (options.db) dbResults = db.lookupJA4(fingerprint.trim());
+              break;
+            case "ja4s":
+              analysis = JA4Analyzer.parseJA4S(fingerprint.trim());
+              if (options.db) dbResults = db.lookupJA4S(fingerprint.trim());
+              break;
+            case "ja4h":
+              analysis = JA4Analyzer.parseJA4H(fingerprint.trim());
+              if (options.db) dbResults = db.lookupJA4H(fingerprint.trim());
+              break;
+            case "ja4x":
+              analysis = JA4Analyzer.parseJA4X(fingerprint.trim());
+              if (options.db) dbResults = db.lookupJA4X(fingerprint.trim());
+              break;
+            case "ja4t":
+              analysis = JA4Analyzer.parseJA4T(fingerprint.trim());
+              if (options.db) dbResults = db.lookupJA4T(fingerprint.trim());
+              break;
+            default:
+              throw new Error(`Unknown fingerprint type: ${options.type}`);
+          }
+
+          results.push({
+            fingerprint: fingerprint.trim(),
+            type: options.type.toUpperCase(),
+            analysis,
+            database: {
+              count: dbResults.length,
+              results: dbResults,
+            },
+          });
+        } catch (error) {
+          results.push({
+            fingerprint: fingerprint.trim(),
+            type: options.type.toUpperCase(),
+            error: (error as Error).message,
+          });
+        }
+      }
+
+      spinner.succeed(`Processed ${fingerprints.length} fingerprints`);
+
+      const output = {
+        summary: {
+          total: fingerprints.length,
+          successful: results.filter((r) => !r.error).length,
+          failed: results.filter((r) => r.error).length,
+          timestamp: new Date().toISOString(),
+        },
+        results,
+      };
+
+      if (options.output) {
+        await fs.writeFile(options.output, JSON.stringify(output, null, 2));
+        console.log(chalk.green(`Results saved to: ${options.output}`));
+      } else {
+        console.log(JSON.stringify(output, null, 2));
+      }
+    } catch (error) {
+      spinner.fail("Batch processing failed");
+      console.error(chalk.red("Error:"), (error as Error).message);
+      process.exit(1);
+    }
+  });
+
+// Global error handler
+process.on("unhandledRejection", (error) => {
+  console.error(chalk.red("Unhandled error:"), error);
+  process.exit(1);
+});
+
+// Parse command line arguments
+program.parse();
index.js
package-lock.json
@@ -0,0 +1,1939 @@
+{
+  "name": "ja4-mcp-server",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "ja4-mcp-server",
+      "version": "0.1.0",
+      "license": "AGPL-3.0-or-later OR LicenseRef-Commercial",
+      "dependencies": {
+        "@modelcontextprotocol/sdk": "^1.0.4",
+        "chalk": "^5.3.0",
+        "commander": "^12.1.0",
+        "ora": "^8.1.0"
+      },
+      "bin": {
+        "ja4": "dist/cli.js",
+        "ja4-mcp-server": "index.js"
+      },
+      "devDependencies": {
+        "@types/node": "24.10.1",
+        "tsx": "^4.19.2",
+        "typescript": "^5.7.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk": {
+      "version": "1.22.0",
+      "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz",
+      "integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.17.1",
+        "ajv-formats": "^3.0.1",
+        "content-type": "^1.0.5",
+        "cors": "^2.8.5",
+        "cross-spawn": "^7.0.5",
+        "eventsource": "^3.0.2",
+        "eventsource-parser": "^3.0.0",
+        "express": "^5.0.1",
+        "express-rate-limit": "^7.5.0",
+        "pkce-challenge": "^5.0.0",
+        "raw-body": "^3.0.0",
+        "zod": "^3.23.8",
+        "zod-to-json-schema": "^3.24.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@cfworker/json-schema": "^4.1.1"
+      },
+      "peerDependenciesMeta": {
+        "@cfworker/json-schema": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "24.10.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+      "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+      "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ajv": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+      "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.0",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.6.3",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.0",
+        "raw-body": "^3.0.0",
+        "type-is": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "5.6.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+      "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/cli-cursor": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+      "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-spinners": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+      "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/commander": {
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+      "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+      "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/eventsource": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+      "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+      "license": "MIT",
+      "dependencies": {
+        "eventsource-parser": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/eventsource-parser": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+      "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+      "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.0",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express-rate-limit": {
+      "version": "7.5.1",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
+      "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/express-rate-limit"
+      },
+      "peerDependencies": {
+        "express": ">= 4.11"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/fast-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+      "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+      "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "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": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-east-asian-width": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+      "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/get-tsconfig": {
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
+      "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-interactive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+      "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+      "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "license": "MIT"
+    },
+    "node_modules/log-symbols": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
+      "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^5.3.0",
+        "is-unicode-supported": "^1.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-symbols/node_modules/is-unicode-supported": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+      "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+      "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/mimic-function": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+      "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+      "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-function": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
+      "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^5.3.0",
+        "cli-cursor": "^5.0.0",
+        "cli-spinners": "^2.9.2",
+        "is-interactive": "^2.0.0",
+        "is-unicode-supported": "^2.0.0",
+        "log-symbols": "^6.0.0",
+        "stdin-discarder": "^0.2.2",
+        "string-width": "^7.2.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+      "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/pkce-challenge": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
+      "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16.20.0"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
+      "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.7.0",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/raw-body/node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/raw-body/node_modules/iconv-lite": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+      "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/raw-body/node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+      "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^7.0.0",
+        "signal-exit": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+      "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.5",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "mime-types": "^3.0.1",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+      "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/stdin-discarder": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
+      "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/tsx": {
+      "version": "4.20.6",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
+      "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "~0.25.0",
+        "get-tsconfig": "^4.7.5"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/zod": {
+      "version": "3.25.76",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+      "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
+    "node_modules/zod-to-json-schema": {
+      "version": "3.25.0",
+      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
+      "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
+      "license": "ISC",
+      "peerDependencies": {
+        "zod": "^3.25 || ^4"
+      }
+    }
+  }
+}
package.json
@@ -5,12 +5,15 @@
   "type": "module",
   "main": "index.js",
   "bin": {
-    "ja4-mcp-server": "./index.js"
+    "ja4-mcp-server": "./index.js",
+    "ja4": "./dist/cli.js"
   },
   "scripts": {
     "start": "node index.js",
     "dev": "node --watch index.js",
-    "test": "node test.js"
+    "test": "node test.js",
+    "build:cli": "tsc cli.ts --outDir dist --target es2022 --module nodenext --moduleResolution nodenext",
+    "cli": "tsx cli.ts"
   },
   "keywords": [
     "ja4",
@@ -25,7 +28,15 @@
   "author": "hrbrmstr",
   "license": "AGPL-3.0-or-later OR LicenseRef-Commercial",
   "dependencies": {
-    "@modelcontextprotocol/sdk": "^1.0.4"
+    "@modelcontextprotocol/sdk": "^1.0.4",
+    "chalk": "^5.3.0",
+    "commander": "^12.1.0",
+    "ora": "^8.1.0"
+  },
+  "devDependencies": {
+    "@types/node": "24.10.1",
+    "tsx": "^4.19.2",
+    "typescript": "^5.7.2"
   },
   "engines": {
     "node": ">=20.0.0"
README.md
@@ -12,11 +12,46 @@ A Model Context Protocol (MCP) server that provides comprehensive analysis of JA
 - **Auto-updating Database**: Daily refresh of fingerprint intelligence from ja4db.com
 - **Human-Readable Output**: Technical but accessible explanations for all components
 - **Analyst Prompts**: Built-in MCP prompts to guide AI assistants in effective JA4+ analysis
+- **Command Line Interface**: Standalone CLI tool for local JA4+ analysis
 
 ## What is JA4+?
 
 JA4+ is a suite of network fingerprinting methods created by FoxIO that are both human and machine-readable. Unlike its predecessor JA3, JA4+ uses a modular `a_b_c` format that allows for:
 
+## Command Line Interface
+
+The project also includes a standalone command-line interface (CLI) tool for analyzing JA4+ fingerprints locally:
+
+```bash
+# Install the CLI globally
+npm install -g
+
+# Analyze a JA4 fingerprint
+ja4 analyze t13d1517h2_8daaf6152771_7128f82b508a
+
+# Compare two fingerprints
+ja4 compare t13d1517h2_8daaf6152771_7128f82b508a t13d1516h2_8daaf6152771_7128f82b508a
+
+# Search the JA4 database
+ja4 search "Chrome"
+
+# Analyze multiple fingerprints from a file
+ja4 batch fingerprints.txt
+
+# Show database statistics
+ja4 stats
+```
+
+For more information about the CLI commands and options, run:
+```bash
+ja4 --help
+ja4 analyze --help
+ja4 compare --help
+ja4 search --help
+ja4 batch --help
+ja4 stats --help
+```
+
 - **Locality-preserving analysis**: Hunt on specific parts (e.g., `JA4_ac` to track actors who vary only cipher selection)
 - **Resistant to evasion**: Sorted ciphers and extensions prevent simple randomization attacks
 - **Multi-protocol support**: TLS (TCP/QUIC), HTTP, SSH, X.509 certificates
tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "outDir": "./dist",
+    "rootDir": ".",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "types": ["node"],
+    "allowSyntheticDefaultImports": true
+  },
+  "include": [
+    "cli.ts"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}