gni commited on
Commit
0e45313
·
0 Parent(s):

Initial commit: Privacy Gateway with Multi-language PII detection (EN/FR) and Docker support

Browse files
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ api/venv/
6
+ .pytest_cache/
7
+
8
+ # Node / Frontend
9
+ node_modules/
10
+ dist/
11
+ dist-ssr/
12
+ *.local
13
+ .vite/
14
+
15
+ # Docker
16
+ .dockerignore
17
+
18
+ # OS
19
+ .DS_Store
20
+ Thumbs.db
21
+
22
+ # Logs
23
+ *.log
24
+ api/tests/__pycache__/
README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PII Moderator Tool
2
+
3
+ A complete toolkit to ensure your personal data never reaches LLM APIs.
4
+
5
+ ## Components
6
+
7
+ 1. **Core API (`/api`)**: FastAPI server using Microsoft Presidio for high-accuracy PII detection and redaction.
8
+ 2. **CLI (`/cli`)**: A developer-friendly tool to redact text or files from the terminal.
9
+ 3. **UI Playground (`/ui`)**: A React-based web interface for real-time visualization of PII redaction.
10
+
11
+ ## Getting Started
12
+
13
+ ### 1. Start the API
14
+ In one terminal:
15
+ ```bash
16
+ cd api
17
+ source venv/bin/activate
18
+ python3 main.py
19
+ ```
20
+
21
+ ### 2. Use the CLI
22
+ In another terminal:
23
+ ```bash
24
+ cd cli
25
+ source ../api/venv/bin/activate
26
+ # Redact a string
27
+ python3 pii_mod.py redact "Contact me at 212-555-0199 or email me at alice@example.com"
28
+ ```
29
+
30
+ ### 3. Launch the UI Playground
31
+ In a third terminal:
32
+ ```bash
33
+ cd ui
34
+ npm run dev
35
+ ```
36
+ Open `http://localhost:5173` in your browser.
37
+
38
+ ## Docker Usage
39
+
40
+ The project is fully dockerized for development and production.
41
+
42
+ ### Development (Hot-reloading enabled)
43
+ ```bash
44
+ docker compose up --build
45
+ ```
46
+ - **API**: `http://localhost:8000`
47
+ - **UI Playground**: `http://localhost:5173`
48
+ - **Python CLI**: `docker compose run cli redact "Some text"`
49
+ - **TypeScript CLI**: `docker compose run cli-ts redact "Some text"`
50
+
51
+ ### Production (Nginx + Optimized images)
52
+ ```bash
53
+ docker compose -f docker-compose.prod.yml up --build
54
+ ```
55
+ - **API**: `http://localhost:8000`
56
+ - **UI Playground**: `http://localhost` (Port 80)
57
+
58
+ - **Context-Aware Redaction**: Uses NLP (spaCy) to understand text context.
59
+ - **Support for many entities**: Names, Emails, Phone Numbers, SSNs, URLs, IP Addresses, and more.
60
+ - **Placeholder Replacement**: Swaps sensitive data with tags like `<PERSON>` or `<EMAIL_ADDRESS>`.
61
+ - **CORS Enabled**: Ready for frontend integration.
api/Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Dockerfile
2
+ FROM python:3.12-slim
3
+
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ build-essential \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Install Python dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Download both English and French spaCy models
17
+ RUN python -m spacy download en_core_web_lg
18
+ RUN python -m spacy download fr_core_news_lg
19
+
20
+ # Copy application code
21
+ COPY main.py .
22
+
23
+ EXPOSE 8000
24
+
25
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
api/main.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import List, Dict, Optional
5
+ import logging
6
+
7
+ from presidio_analyzer import AnalyzerEngine, RecognizerRegistry, PatternRecognizer, Pattern
8
+ from presidio_analyzer.predefined_recognizers import SpacyRecognizer
9
+ from presidio_analyzer.nlp_engine import NlpEngineProvider
10
+ from presidio_anonymizer import AnonymizerEngine
11
+ from langdetect import detect, DetectorFactory
12
+ import uvicorn
13
+
14
+ # Setup logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ DetectorFactory.seed = 0
19
+
20
+ app = FastAPI(title="Privacy Gateway Professional")
21
+
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"],
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ # 1. Configuration NLP Engine avec mappage labels FR/EN
31
+ configuration = {
32
+ "nlp_engine_name": "spacy",
33
+ "models": [
34
+ {"lang_code": "en", "model_name": "en_core_web_lg"},
35
+ {"lang_code": "fr", "model_name": "fr_core_news_lg"}
36
+ ],
37
+ "ner_model_configuration": {
38
+ "model_to_presidio_entity_mapping": {
39
+ "PER": "PERSON",
40
+ "PERSON": "PERSON",
41
+ "LOC": "LOCATION",
42
+ "GPE": "LOCATION",
43
+ "ORG": "ORGANIZATION",
44
+ }
45
+ }
46
+ }
47
+
48
+ provider = NlpEngineProvider(nlp_configuration=configuration)
49
+ nlp_engine = provider.create_engine()
50
+
51
+ # 2. Setup Registry
52
+ registry = RecognizerRegistry()
53
+ registry.load_predefined_recognizers(languages=["en", "fr"])
54
+
55
+ # Forcer le mappage spaCy pour le Français
56
+ fr_spacy = SpacyRecognizer(
57
+ supported_language="fr",
58
+ check_label_groups=[
59
+ ("PERSON", ["PER", "PERSON"]),
60
+ ("LOCATION", ["LOC", "GPE", "LOCATION"]),
61
+ ("ORGANIZATION", ["ORG", "ORGANIZATION"])
62
+ ]
63
+ )
64
+ registry.add_recognizer(fr_spacy)
65
+
66
+ # --- CUSTOM EXPERT RECOGNIZERS ---
67
+
68
+ # French Addresses (Capture large pour la rue et la ville)
69
+ registry.add_recognizer(PatternRecognizer(
70
+ supported_entity="LOCATION",
71
+ supported_language="fr",
72
+ patterns=[Pattern(name="address", regex=r"(?i)\b\d{1,4}[\s,]+(?:rue|av|ave|avenue|bd|boulevard|impasse|place|square|quai|cours|passage|route|chemin)[\s\w\-\'àâäéèêëîïôöùûüç,]{2,100}\b", score=0.85)],
73
+ context=["habite", "adresse", "réside", "domicilié"]
74
+ ))
75
+
76
+ # SIRET
77
+ registry.add_recognizer(PatternRecognizer(
78
+ supported_entity="SIRET",
79
+ supported_language="fr",
80
+ patterns=[Pattern(name="siret", regex=r"\b\d{3}\s*\d{3}\s*\d{3}\s*\d{5}\b", score=0.95)],
81
+ context=["siret", "entreprise", "société"]
82
+ ))
83
+
84
+ # NIR
85
+ registry.add_recognizer(PatternRecognizer(
86
+ supported_entity="FR_NIR",
87
+ supported_language="fr",
88
+ patterns=[Pattern(name="nir", regex=r"\b[12]\s*\d{2}\s*\d{2}\s*(?:\d{2}|2[AB])\s*\d{3}\s*\d{3}\s*\d{2}\b", score=0.95)],
89
+ context=["sécurité sociale", "nir", "assuré"]
90
+ ))
91
+
92
+ # French Phones
93
+ registry.add_recognizer(PatternRecognizer(
94
+ supported_entity="PHONE_NUMBER",
95
+ supported_language="fr",
96
+ patterns=[Pattern(name="fr_phone", regex=r"(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}", score=0.85)],
97
+ context=["téléphone", "tél", "mobile", "portable"]
98
+ ))
99
+
100
+ # 3. Initialize Analyzer (Seuil stable 0.3)
101
+ analyzer = AnalyzerEngine(
102
+ nlp_engine=nlp_engine,
103
+ registry=registry,
104
+ default_score_threshold=0.3
105
+ )
106
+ anonymizer = AnonymizerEngine()
107
+
108
+ class RedactRequest(BaseModel):
109
+ text: str
110
+ language: Optional[str] = "auto"
111
+
112
+ @app.get("/")
113
+ async def root():
114
+ return {"status": "online", "mode": "professional"}
115
+
116
+ @app.post("/redact")
117
+ async def redact_text(request: RedactRequest):
118
+ try:
119
+ # Detect language
120
+ try:
121
+ target_lang = detect(request.text) if request.language == "auto" else request.language
122
+ if target_lang not in ["en", "fr"]: target_lang = "en"
123
+ except:
124
+ target_lang = "en"
125
+
126
+ # Analyze
127
+ results = analyzer.analyze(text=request.text, language=target_lang)
128
+
129
+ # Anonymize
130
+ anonymized = anonymizer.anonymize(text=request.text, analyzer_results=results)
131
+
132
+ return {
133
+ "original_text": request.text,
134
+ "redacted_text": anonymized.text,
135
+ "detected_language": target_lang,
136
+ "detected_entities": [{"entity_type": res.entity_type, "score": res.score} for res in results]
137
+ }
138
+ except Exception as e:
139
+ logger.error(f"Error: {str(e)}")
140
+ raise HTTPException(status_code=500, detail=str(e))
141
+
142
+ if __name__ == "__main__":
143
+ uvicorn.run(app, host="0.0.0.0", port=8000)
api/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.135.1
2
+ uvicorn==0.42.0
3
+ presidio-analyzer==2.2.362
4
+ presidio-anonymizer==2.2.362
5
+ spacy==3.8.11
6
+ requests==2.32.5
7
+ langdetect==1.0.9
api/test_final.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ def ironclad_nuclear_redact(text: str) -> str:
4
+ redacted = text
5
+ # 1. Numbers (Aggressive 9+)
6
+ numbers_regex = r"\b\d(?:[\s.-]*\d){8,45}\b"
7
+ redacted = re.sub(numbers_regex, "<SECURE_NUMBER>", redacted)
8
+ # 2. Quotes
9
+ redacted = re.sub(r"[\"']([^\"']{3,})[\"']", "<ORGANIZATION>", redacted)
10
+ # 3. Capitalized Groups
11
+ name_regex = r"(?<![m|l|d|j|s|n]\')\b[A-ZÀ-Ÿ][a-zà-ÿ]+(?:[\s-][A-ZÀ-Ÿ][a-zà-ÿ]+)+\b"
12
+ redacted = re.sub(name_regex, "<PII_DATA>", redacted)
13
+ # 4. Mid-sentence Capitalized
14
+ city_regex = r"(?<![.!?])\s+\b([A-ZÀ-Ÿ][a-zà-ÿ]{2,})\b"
15
+ redacted = re.sub(city_regex, " <PII_DATA>", redacted)
16
+ return redacted
17
+
18
+ def test_final():
19
+ test_cases = [
20
+ {
21
+ "name": "French Professional",
22
+ "text": "Monsieur Bernard Petit travaille chez \"Global Import Export\". Il habite au 42 bis, rue des Lilas à Lyon (69000). Son SIREN est le 123 456 789. Contact: 07-88-99-00-11."
23
+ },
24
+ {
25
+ "name": "English Medical",
26
+ "text": "Patient Sarah Jenkins admitted to 'St. Jude Hospital'. Address: 789 Healthcare Blvd, San Francisco. SSN: 123-45-6789."
27
+ }
28
+ ]
29
+
30
+ for case in test_cases:
31
+ print(f"\n--- Testing {case['name']} ---")
32
+ final = ironclad_nuclear_redact(case['text'])
33
+ print(f"Result: {final}")
34
+
35
+ assert "Bernard Petit" not in final
36
+ assert "Global Import Export" not in final
37
+ assert "Lyon" not in final
38
+ assert "123 456 789" not in final
39
+ assert "Sarah Jenkins" not in final
40
+ assert "St. Jude Hospital" not in final
41
+ assert "San Francisco" not in final
42
+ assert "123-45-6789" not in final
43
+
44
+ print("\n✅ NUCLEAR PROTECTION VERIFIED 100% ON NEW DATA!")
45
+
46
+ if __name__ == "__main__":
47
+ test_final()
api/test_logic.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from presidio_analyzer import AnalyzerEngine
2
+ from presidio_anonymizer import AnonymizerEngine
3
+
4
+ def test_pii_logic():
5
+ analyzer = AnalyzerEngine()
6
+ anonymizer = AnonymizerEngine()
7
+
8
+ test_text = "My name is Alice and my phone number is 212-555-0100"
9
+
10
+ # 1. Analyze
11
+ results = analyzer.analyze(text=test_text, language='en')
12
+ print(f"Detected {len(results)} entities.")
13
+
14
+ # 2. Anonymize
15
+ anonymized = anonymizer.anonymize(text=test_text, analyzer_results=results)
16
+
17
+ print(f"Original: {test_text}")
18
+ print(f"Redacted: {anonymized.text}")
19
+
20
+ # Simple assertions
21
+ assert "Alice" not in anonymized.text
22
+ assert "<PERSON>" in anonymized.text or "PERSON" in anonymized.text
23
+ print("Test passed successfully!")
24
+
25
+ if __name__ == "__main__":
26
+ test_pii_logic()
api/tests/__init__.py ADDED
File without changes
api/tests/test_suite.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import re
4
+ import pytest
5
+ from presidio_analyzer import AnalyzerEngine, RecognizerRegistry, PatternRecognizer, Pattern
6
+ from presidio_analyzer.predefined_recognizers import SpacyRecognizer
7
+ from presidio_analyzer.nlp_engine import NlpEngineProvider
8
+ from presidio_anonymizer import AnonymizerEngine
9
+
10
+ def get_engines():
11
+ # 1. Moteur NLP avec mappage explicite
12
+ configuration = {
13
+ "nlp_engine_name": "spacy",
14
+ "models": [{"lang_code": "en", "model_name": "en_core_web_lg"}, {"lang_code": "fr", "model_name": "fr_core_news_lg"}]
15
+ }
16
+ provider = NlpEngineProvider(nlp_configuration=configuration)
17
+ nlp_engine = provider.create_engine()
18
+
19
+ # 2. Registre
20
+ registry = RecognizerRegistry()
21
+ registry.load_predefined_recognizers(languages=["en", "fr"])
22
+
23
+ # --- SOLUTION : SpacyRecognizer forcé pour le Français ---
24
+ fr_spacy = SpacyRecognizer(
25
+ supported_language="fr",
26
+ check_label_groups=[
27
+ ("PERSON", ["PER", "PERSON"]),
28
+ ("LOCATION", ["LOC", "GPE", "LOCATION"]),
29
+ ("ORGANIZATION", ["ORG", "ORGANIZATION"])
30
+ ]
31
+ )
32
+ registry.add_recognizer(fr_spacy)
33
+
34
+ # Custom FR Recognizers
35
+ registry.add_recognizer(PatternRecognizer(supported_entity="LOCATION", supported_language="fr", patterns=[Pattern(name="address", regex=r"(?i)\b\d{1,4}[\s,]+(?:rue|av|ave|avenue|bd|boulevard|impasse|place|square|quai|cours|passage|route|chemin)[\s\w\-\'àâäéèêëîïôöùûüç,]{2,100}\b", score=0.85)], context=["habite", "adresse", "réside"]))
36
+ registry.add_recognizer(PatternRecognizer(supported_entity="SIRET", supported_language="fr", patterns=[Pattern(name="siret", regex=r"\b\d{3}\s*\d{3}\s*\d{3}\s*\d{5}\b", score=0.95)], context=["siret"]))
37
+ registry.add_recognizer(PatternRecognizer(supported_entity="FR_NIR", supported_language="fr", patterns=[Pattern(name="nir", regex=r"\b[12]\s*\d{2}\s*\d{2}\s*(?:\d{2}|2[AB])\s*\d{3}\s*\d{3}\s*\d{2}\b", score=0.95)], context=["sécurité sociale"]))
38
+ registry.add_recognizer(PatternRecognizer(supported_entity="PHONE_NUMBER", supported_language="fr", patterns=[Pattern(name="fr_phone", regex=r"(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}", score=0.85)], context=["téléphone", "tél"]))
39
+
40
+ # Seuil 0.25 pour ne rien rater
41
+ analyzer = AnalyzerEngine(nlp_engine=nlp_engine, registry=registry, default_score_threshold=0.25)
42
+ anonymizer = AnonymizerEngine()
43
+ return analyzer, anonymizer
44
+
45
+ def test_comprehensive_fr():
46
+ analyzer, anonymizer = get_engines()
47
+ text = "Jean Dupont habite au 12, rue de la Paix à Paris. Son SIRET est 123 456 789 00012 et son tél est 0612345678."
48
+ results = analyzer.analyze(text=text, language="fr")
49
+
50
+ print("\nEntities detected:")
51
+ for r in results:
52
+ print(f" - {r.entity_type}: '{text[r.start:r.end]}' ({r.score})")
53
+
54
+ redacted = anonymizer.anonymize(text=text, analyzer_results=results).text
55
+ print(f"Result: {redacted}")
56
+
57
+ assert "Jean Dupont" not in redacted
58
+ assert "12, rue de la Paix" not in redacted
59
+ assert "Paris" not in redacted
60
+ assert "123 456 789 00012" not in redacted
61
+ assert "0612345678" not in redacted
62
+
63
+ if __name__ == "__main__":
64
+ try:
65
+ test_comprehensive_fr()
66
+ print("\n✅ FRENCH COMPREHENSIVE PASSED!")
67
+ except AssertionError:
68
+ print("\n❌ TEST FAILED")
69
+ sys.exit(1)
cli-ts/Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLI TS Dockerfile
2
+ FROM node:25-slim
3
+
4
+ WORKDIR /app
5
+
6
+ COPY package*.json ./
7
+ RUN npm install
8
+
9
+ COPY . .
10
+ RUN npm run build
11
+
12
+ # Set executable permissions
13
+ RUN chmod +x dist/index.js
14
+
15
+ # Remove ENTRYPOINT to allow docker-compose command to run sh
16
+ CMD ["node", "--no-warnings", "dist/index.js"]
cli-ts/index.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Command } from 'commander';
2
+ import axios from 'axios';
3
+ import chalk from 'chalk';
4
+ import * as fs from 'fs';
5
+
6
+ const program = new Command();
7
+ const API_URL = process.env.API_URL || 'http://localhost:8000';
8
+
9
+ program
10
+ .name('pii-ts')
11
+ .description('TypeScript PII Moderator CLI')
12
+ .version('1.0.0');
13
+
14
+ program
15
+ .command('redact')
16
+ .description('Redact PII from a string')
17
+ .argument('<text>', 'The text to redact')
18
+ .action(async (text: string) => {
19
+ try {
20
+ const response = await axios.post(`${API_URL}/redact`, { text });
21
+ const { original_text, redacted_text, detected_entities } = response.data;
22
+
23
+ console.log(chalk.cyan('\nOriginal:'));
24
+ console.log(original_text);
25
+
26
+ console.log(chalk.green('\nRedacted:'));
27
+ console.log(redacted_text);
28
+
29
+ if (detected_entities && detected_entities.length > 0) {
30
+ console.log(chalk.yellow('\nDetected Entities:'));
31
+ detected_entities.forEach((ent: any) => {
32
+ console.log(`- ${ent.entity_type} (Score: ${ent.score.toFixed(2)})`);
33
+ });
34
+ }
35
+ } catch (error: any) {
36
+ console.error(chalk.red(`Error: ${error.message}`));
37
+ process.exit(1);
38
+ }
39
+ });
40
+
41
+ program
42
+ .command('redact-file')
43
+ .description('Redact PII from a file')
44
+ .argument('<path>', 'Path to the file')
45
+ .action(async (path: string) => {
46
+ try {
47
+ if (!fs.existsSync(path)) {
48
+ console.error(chalk.red(`Error: File not found at ${path}`));
49
+ process.exit(1);
50
+ }
51
+ const content = fs.readFileSync(path, 'utf-8');
52
+ const response = await axios.post(`${API_URL}/redact`, { text: content });
53
+ console.log(response.data.redacted_text);
54
+ } catch (error: any) {
55
+ console.error(chalk.red(`Error: ${error.message}`));
56
+ process.exit(1);
57
+ }
58
+ });
59
+
60
+ program.parse();
61
+
62
+ if (!process.argv.slice(2).length) {
63
+ program.outputHelp();
64
+ }
cli-ts/package-lock.json ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cli-ts",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "cli-ts",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "axios": "^1.13.6",
13
+ "chalk": "^5.6.2",
14
+ "commander": "^14.0.3"
15
+ },
16
+ "devDependencies": {
17
+ "@types/axios": "^0.9.36",
18
+ "@types/commander": "^2.12.0",
19
+ "@types/node": "^25.5.0",
20
+ "ts-node": "^10.9.2",
21
+ "typescript": "^5.9.3"
22
+ }
23
+ },
24
+ "node_modules/@cspotcode/source-map-support": {
25
+ "version": "0.8.1",
26
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
27
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
28
+ "dev": true,
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@jridgewell/trace-mapping": "0.3.9"
32
+ },
33
+ "engines": {
34
+ "node": ">=12"
35
+ }
36
+ },
37
+ "node_modules/@jridgewell/resolve-uri": {
38
+ "version": "3.1.2",
39
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
40
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
41
+ "dev": true,
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=6.0.0"
45
+ }
46
+ },
47
+ "node_modules/@jridgewell/sourcemap-codec": {
48
+ "version": "1.5.5",
49
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
50
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
51
+ "dev": true,
52
+ "license": "MIT"
53
+ },
54
+ "node_modules/@jridgewell/trace-mapping": {
55
+ "version": "0.3.9",
56
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
57
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
58
+ "dev": true,
59
+ "license": "MIT",
60
+ "dependencies": {
61
+ "@jridgewell/resolve-uri": "^3.0.3",
62
+ "@jridgewell/sourcemap-codec": "^1.4.10"
63
+ }
64
+ },
65
+ "node_modules/@tsconfig/node10": {
66
+ "version": "1.0.12",
67
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
68
+ "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
69
+ "dev": true,
70
+ "license": "MIT"
71
+ },
72
+ "node_modules/@tsconfig/node12": {
73
+ "version": "1.0.11",
74
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
75
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
76
+ "dev": true,
77
+ "license": "MIT"
78
+ },
79
+ "node_modules/@tsconfig/node14": {
80
+ "version": "1.0.3",
81
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
82
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
83
+ "dev": true,
84
+ "license": "MIT"
85
+ },
86
+ "node_modules/@tsconfig/node16": {
87
+ "version": "1.0.4",
88
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
89
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
90
+ "dev": true,
91
+ "license": "MIT"
92
+ },
93
+ "node_modules/@types/axios": {
94
+ "version": "0.9.36",
95
+ "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz",
96
+ "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==",
97
+ "dev": true,
98
+ "license": "MIT"
99
+ },
100
+ "node_modules/@types/commander": {
101
+ "version": "2.12.0",
102
+ "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.0.tgz",
103
+ "integrity": "sha512-DDmRkovH7jPjnx7HcbSnqKg2JeNANyxNZeUvB0iE+qKBLN+vzN5iSIwt+J2PFSmBuYEut4mgQvI/fTX9YQH/vw==",
104
+ "dev": true,
105
+ "license": "MIT",
106
+ "dependencies": {
107
+ "commander": "*"
108
+ }
109
+ },
110
+ "node_modules/@types/node": {
111
+ "version": "25.5.0",
112
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
113
+ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
114
+ "dev": true,
115
+ "license": "MIT",
116
+ "dependencies": {
117
+ "undici-types": "~7.18.0"
118
+ }
119
+ },
120
+ "node_modules/acorn": {
121
+ "version": "8.16.0",
122
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
123
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
124
+ "dev": true,
125
+ "license": "MIT",
126
+ "bin": {
127
+ "acorn": "bin/acorn"
128
+ },
129
+ "engines": {
130
+ "node": ">=0.4.0"
131
+ }
132
+ },
133
+ "node_modules/acorn-walk": {
134
+ "version": "8.3.5",
135
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
136
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
137
+ "dev": true,
138
+ "license": "MIT",
139
+ "dependencies": {
140
+ "acorn": "^8.11.0"
141
+ },
142
+ "engines": {
143
+ "node": ">=0.4.0"
144
+ }
145
+ },
146
+ "node_modules/arg": {
147
+ "version": "4.1.3",
148
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
149
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
150
+ "dev": true,
151
+ "license": "MIT"
152
+ },
153
+ "node_modules/asynckit": {
154
+ "version": "0.4.0",
155
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
156
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
157
+ "license": "MIT"
158
+ },
159
+ "node_modules/axios": {
160
+ "version": "1.13.6",
161
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
162
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
163
+ "license": "MIT",
164
+ "dependencies": {
165
+ "follow-redirects": "^1.15.11",
166
+ "form-data": "^4.0.5",
167
+ "proxy-from-env": "^1.1.0"
168
+ }
169
+ },
170
+ "node_modules/call-bind-apply-helpers": {
171
+ "version": "1.0.2",
172
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
173
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
174
+ "license": "MIT",
175
+ "dependencies": {
176
+ "es-errors": "^1.3.0",
177
+ "function-bind": "^1.1.2"
178
+ },
179
+ "engines": {
180
+ "node": ">= 0.4"
181
+ }
182
+ },
183
+ "node_modules/chalk": {
184
+ "version": "5.6.2",
185
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
186
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
187
+ "license": "MIT",
188
+ "engines": {
189
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
190
+ },
191
+ "funding": {
192
+ "url": "https://github.com/chalk/chalk?sponsor=1"
193
+ }
194
+ },
195
+ "node_modules/combined-stream": {
196
+ "version": "1.0.8",
197
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
198
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
199
+ "license": "MIT",
200
+ "dependencies": {
201
+ "delayed-stream": "~1.0.0"
202
+ },
203
+ "engines": {
204
+ "node": ">= 0.8"
205
+ }
206
+ },
207
+ "node_modules/commander": {
208
+ "version": "14.0.3",
209
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
210
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
211
+ "license": "MIT",
212
+ "engines": {
213
+ "node": ">=20"
214
+ }
215
+ },
216
+ "node_modules/create-require": {
217
+ "version": "1.1.1",
218
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
219
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
220
+ "dev": true,
221
+ "license": "MIT"
222
+ },
223
+ "node_modules/delayed-stream": {
224
+ "version": "1.0.0",
225
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
226
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
227
+ "license": "MIT",
228
+ "engines": {
229
+ "node": ">=0.4.0"
230
+ }
231
+ },
232
+ "node_modules/diff": {
233
+ "version": "4.0.4",
234
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
235
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
236
+ "dev": true,
237
+ "license": "BSD-3-Clause",
238
+ "engines": {
239
+ "node": ">=0.3.1"
240
+ }
241
+ },
242
+ "node_modules/dunder-proto": {
243
+ "version": "1.0.1",
244
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
245
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
246
+ "license": "MIT",
247
+ "dependencies": {
248
+ "call-bind-apply-helpers": "^1.0.1",
249
+ "es-errors": "^1.3.0",
250
+ "gopd": "^1.2.0"
251
+ },
252
+ "engines": {
253
+ "node": ">= 0.4"
254
+ }
255
+ },
256
+ "node_modules/es-define-property": {
257
+ "version": "1.0.1",
258
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
259
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
260
+ "license": "MIT",
261
+ "engines": {
262
+ "node": ">= 0.4"
263
+ }
264
+ },
265
+ "node_modules/es-errors": {
266
+ "version": "1.3.0",
267
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
268
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
269
+ "license": "MIT",
270
+ "engines": {
271
+ "node": ">= 0.4"
272
+ }
273
+ },
274
+ "node_modules/es-object-atoms": {
275
+ "version": "1.1.1",
276
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
277
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
278
+ "license": "MIT",
279
+ "dependencies": {
280
+ "es-errors": "^1.3.0"
281
+ },
282
+ "engines": {
283
+ "node": ">= 0.4"
284
+ }
285
+ },
286
+ "node_modules/es-set-tostringtag": {
287
+ "version": "2.1.0",
288
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
289
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
290
+ "license": "MIT",
291
+ "dependencies": {
292
+ "es-errors": "^1.3.0",
293
+ "get-intrinsic": "^1.2.6",
294
+ "has-tostringtag": "^1.0.2",
295
+ "hasown": "^2.0.2"
296
+ },
297
+ "engines": {
298
+ "node": ">= 0.4"
299
+ }
300
+ },
301
+ "node_modules/follow-redirects": {
302
+ "version": "1.15.11",
303
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
304
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
305
+ "funding": [
306
+ {
307
+ "type": "individual",
308
+ "url": "https://github.com/sponsors/RubenVerborgh"
309
+ }
310
+ ],
311
+ "license": "MIT",
312
+ "engines": {
313
+ "node": ">=4.0"
314
+ },
315
+ "peerDependenciesMeta": {
316
+ "debug": {
317
+ "optional": true
318
+ }
319
+ }
320
+ },
321
+ "node_modules/form-data": {
322
+ "version": "4.0.5",
323
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
324
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
325
+ "license": "MIT",
326
+ "dependencies": {
327
+ "asynckit": "^0.4.0",
328
+ "combined-stream": "^1.0.8",
329
+ "es-set-tostringtag": "^2.1.0",
330
+ "hasown": "^2.0.2",
331
+ "mime-types": "^2.1.12"
332
+ },
333
+ "engines": {
334
+ "node": ">= 6"
335
+ }
336
+ },
337
+ "node_modules/function-bind": {
338
+ "version": "1.1.2",
339
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
340
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
341
+ "license": "MIT",
342
+ "funding": {
343
+ "url": "https://github.com/sponsors/ljharb"
344
+ }
345
+ },
346
+ "node_modules/get-intrinsic": {
347
+ "version": "1.3.0",
348
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
349
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
350
+ "license": "MIT",
351
+ "dependencies": {
352
+ "call-bind-apply-helpers": "^1.0.2",
353
+ "es-define-property": "^1.0.1",
354
+ "es-errors": "^1.3.0",
355
+ "es-object-atoms": "^1.1.1",
356
+ "function-bind": "^1.1.2",
357
+ "get-proto": "^1.0.1",
358
+ "gopd": "^1.2.0",
359
+ "has-symbols": "^1.1.0",
360
+ "hasown": "^2.0.2",
361
+ "math-intrinsics": "^1.1.0"
362
+ },
363
+ "engines": {
364
+ "node": ">= 0.4"
365
+ },
366
+ "funding": {
367
+ "url": "https://github.com/sponsors/ljharb"
368
+ }
369
+ },
370
+ "node_modules/get-proto": {
371
+ "version": "1.0.1",
372
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
373
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
374
+ "license": "MIT",
375
+ "dependencies": {
376
+ "dunder-proto": "^1.0.1",
377
+ "es-object-atoms": "^1.0.0"
378
+ },
379
+ "engines": {
380
+ "node": ">= 0.4"
381
+ }
382
+ },
383
+ "node_modules/gopd": {
384
+ "version": "1.2.0",
385
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
386
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
387
+ "license": "MIT",
388
+ "engines": {
389
+ "node": ">= 0.4"
390
+ },
391
+ "funding": {
392
+ "url": "https://github.com/sponsors/ljharb"
393
+ }
394
+ },
395
+ "node_modules/has-symbols": {
396
+ "version": "1.1.0",
397
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
398
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
399
+ "license": "MIT",
400
+ "engines": {
401
+ "node": ">= 0.4"
402
+ },
403
+ "funding": {
404
+ "url": "https://github.com/sponsors/ljharb"
405
+ }
406
+ },
407
+ "node_modules/has-tostringtag": {
408
+ "version": "1.0.2",
409
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
410
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
411
+ "license": "MIT",
412
+ "dependencies": {
413
+ "has-symbols": "^1.0.3"
414
+ },
415
+ "engines": {
416
+ "node": ">= 0.4"
417
+ },
418
+ "funding": {
419
+ "url": "https://github.com/sponsors/ljharb"
420
+ }
421
+ },
422
+ "node_modules/hasown": {
423
+ "version": "2.0.2",
424
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
425
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
426
+ "license": "MIT",
427
+ "dependencies": {
428
+ "function-bind": "^1.1.2"
429
+ },
430
+ "engines": {
431
+ "node": ">= 0.4"
432
+ }
433
+ },
434
+ "node_modules/make-error": {
435
+ "version": "1.3.6",
436
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
437
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
438
+ "dev": true,
439
+ "license": "ISC"
440
+ },
441
+ "node_modules/math-intrinsics": {
442
+ "version": "1.1.0",
443
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
444
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
445
+ "license": "MIT",
446
+ "engines": {
447
+ "node": ">= 0.4"
448
+ }
449
+ },
450
+ "node_modules/mime-db": {
451
+ "version": "1.52.0",
452
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
453
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
454
+ "license": "MIT",
455
+ "engines": {
456
+ "node": ">= 0.6"
457
+ }
458
+ },
459
+ "node_modules/mime-types": {
460
+ "version": "2.1.35",
461
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
462
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
463
+ "license": "MIT",
464
+ "dependencies": {
465
+ "mime-db": "1.52.0"
466
+ },
467
+ "engines": {
468
+ "node": ">= 0.6"
469
+ }
470
+ },
471
+ "node_modules/proxy-from-env": {
472
+ "version": "1.1.0",
473
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
474
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
475
+ "license": "MIT"
476
+ },
477
+ "node_modules/ts-node": {
478
+ "version": "10.9.2",
479
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
480
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
481
+ "dev": true,
482
+ "license": "MIT",
483
+ "dependencies": {
484
+ "@cspotcode/source-map-support": "^0.8.0",
485
+ "@tsconfig/node10": "^1.0.7",
486
+ "@tsconfig/node12": "^1.0.7",
487
+ "@tsconfig/node14": "^1.0.0",
488
+ "@tsconfig/node16": "^1.0.2",
489
+ "acorn": "^8.4.1",
490
+ "acorn-walk": "^8.1.1",
491
+ "arg": "^4.1.0",
492
+ "create-require": "^1.1.0",
493
+ "diff": "^4.0.1",
494
+ "make-error": "^1.1.1",
495
+ "v8-compile-cache-lib": "^3.0.1",
496
+ "yn": "3.1.1"
497
+ },
498
+ "bin": {
499
+ "ts-node": "dist/bin.js",
500
+ "ts-node-cwd": "dist/bin-cwd.js",
501
+ "ts-node-esm": "dist/bin-esm.js",
502
+ "ts-node-script": "dist/bin-script.js",
503
+ "ts-node-transpile-only": "dist/bin-transpile.js",
504
+ "ts-script": "dist/bin-script-deprecated.js"
505
+ },
506
+ "peerDependencies": {
507
+ "@swc/core": ">=1.2.50",
508
+ "@swc/wasm": ">=1.2.50",
509
+ "@types/node": "*",
510
+ "typescript": ">=2.7"
511
+ },
512
+ "peerDependenciesMeta": {
513
+ "@swc/core": {
514
+ "optional": true
515
+ },
516
+ "@swc/wasm": {
517
+ "optional": true
518
+ }
519
+ }
520
+ },
521
+ "node_modules/typescript": {
522
+ "version": "5.9.3",
523
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
524
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
525
+ "dev": true,
526
+ "license": "Apache-2.0",
527
+ "bin": {
528
+ "tsc": "bin/tsc",
529
+ "tsserver": "bin/tsserver"
530
+ },
531
+ "engines": {
532
+ "node": ">=14.17"
533
+ }
534
+ },
535
+ "node_modules/undici-types": {
536
+ "version": "7.18.2",
537
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
538
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
539
+ "dev": true,
540
+ "license": "MIT"
541
+ },
542
+ "node_modules/v8-compile-cache-lib": {
543
+ "version": "3.0.1",
544
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
545
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
546
+ "dev": true,
547
+ "license": "MIT"
548
+ },
549
+ "node_modules/yn": {
550
+ "version": "3.1.1",
551
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
552
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
553
+ "dev": true,
554
+ "license": "MIT",
555
+ "engines": {
556
+ "node": ">=6"
557
+ }
558
+ }
559
+ }
560
+ }
cli-ts/package.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pii-ts-cli",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript PII Moderator CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "pii-ts": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node --no-warnings dist/index.js",
12
+ "dev": "ts-node --no-warnings index.ts"
13
+ },
14
+ "dependencies": {
15
+ "axios": "^1.8.1",
16
+ "chalk": "^5.4.1",
17
+ "commander": "^13.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^24.12.0",
21
+ "ts-node": "^10.9.2",
22
+ "typescript": "^5.8.2"
23
+ }
24
+ }
cli-ts/tsconfig.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "outDir": "./dist",
7
+ "rootDir": "./",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true
12
+ },
13
+ "include": ["index.ts"]
14
+ }
cli/Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLI Dockerfile
2
+ FROM python:3.12-slim
3
+
4
+ WORKDIR /app
5
+
6
+ # Install dependencies
7
+ RUN pip install --no-cache-dir click requests
8
+
9
+ # Copy CLI script
10
+ COPY pii_mod.py .
11
+
12
+ # Use CMD instead of ENTRYPOINT to allow docker-compose command
13
+ CMD ["python", "pii_mod.py"]
cli/pii_mod.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import click
2
+ import requests
3
+ import json
4
+ import sys
5
+
6
+ API_URL = "http://localhost:8000"
7
+
8
+ @click.group()
9
+ def cli():
10
+ """PII Moderator CLI - Protect your data before sending it to LLMs."""
11
+ pass
12
+
13
+ @cli.command()
14
+ @click.argument('text')
15
+ @click.option('--entities', '-e', multiple=True, help="Specific entities to redact (e.g. -e PERSON -e EMAIL_ADDRESS)")
16
+ def redact(text, entities):
17
+ """Redact PII from a given string."""
18
+ try:
19
+ payload = {"text": text}
20
+ if entities:
21
+ payload["entities"] = list(entities)
22
+
23
+ response = requests.post(f"{API_URL}/redact", json=payload)
24
+ response.raise_for_status()
25
+
26
+ result = response.json()
27
+ click.echo(click.style("\nOriginal:", fg="cyan"))
28
+ click.echo(result["original_text"])
29
+ click.echo(click.style("\nRedacted:", fg="green"))
30
+ click.echo(result["redacted_text"])
31
+
32
+ if result["detected_entities"]:
33
+ click.echo(click.style("\nDetected Entities:", fg="yellow"))
34
+ for ent in result["detected_entities"]:
35
+ click.echo(f"- {ent['entity_type']} (Score: {ent['score']:.2f})")
36
+
37
+ except Exception as e:
38
+ click.echo(click.style(f"Error: {e}", fg="red"), err=True)
39
+ sys.exit(1)
40
+
41
+ @cli.command()
42
+ @click.argument('file', type=click.File('r'))
43
+ def redact_file(file):
44
+ """Redact PII from a file."""
45
+ content = file.read()
46
+ try:
47
+ payload = {"text": content}
48
+ response = requests.post(f"{API_URL}/redact", json=payload)
49
+ response.raise_for_status()
50
+ result = response.json()
51
+ click.echo(result["redacted_text"])
52
+ except Exception as e:
53
+ click.echo(click.style(f"Error: {e}", fg="red"), err=True)
54
+ sys.exit(1)
55
+
56
+ if __name__ == "__main__":
57
+ cli()
docker-compose.prod.yml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docker Compose for Production
2
+ version: '3.8'
3
+
4
+ services:
5
+ # 1. API Production
6
+ api:
7
+ build:
8
+ context: ./api
9
+ dockerfile: Dockerfile
10
+ ports:
11
+ - "8000:8000"
12
+ restart: always
13
+
14
+ # 2. Web UI Production (Nginx)
15
+ ui:
16
+ build:
17
+ context: ./ui
18
+ dockerfile: Dockerfile
19
+ target: prod
20
+ ports:
21
+ - "80:80"
22
+ depends_on:
23
+ - api
24
+ restart: always
docker-compose.yml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docker Compose for Development
2
+ version: '3.8'
3
+
4
+ services:
5
+ # 1. API Service (Core Moderator)
6
+ api:
7
+ build:
8
+ context: ./api
9
+ dockerfile: Dockerfile
10
+ ports:
11
+ - "8000:8000"
12
+ volumes:
13
+ - ./api:/app
14
+ command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
15
+
16
+ # 2. Web UI Playground
17
+ ui:
18
+ build:
19
+ context: ./ui
20
+ dockerfile: Dockerfile
21
+ target: dev
22
+ ports:
23
+ - "5173:5173"
24
+ volumes:
25
+ - ./ui:/app
26
+ - /app/node_modules
27
+ depends_on:
28
+ - api
29
+ environment:
30
+ - VITE_API_URL=http://localhost:8000
31
+ command: sh -c "npm install && npm run dev -- --host"
32
+
33
+ # 3. Python CLI
34
+ cli:
35
+ build:
36
+ context: ./cli
37
+ dockerfile: Dockerfile
38
+ volumes:
39
+ - ./cli:/app
40
+ environment:
41
+ - API_URL=http://api:8000
42
+ depends_on:
43
+ - api
44
+
45
+ # 4. TypeScript CLI
46
+ cli-ts:
47
+ build:
48
+ context: ./cli-ts
49
+ dockerfile: Dockerfile
50
+ volumes:
51
+ - ./cli-ts:/app
52
+ - /app/node_modules
53
+ - /app/dist
54
+ environment:
55
+ - API_URL=http://api:8000
56
+ depends_on:
57
+ - api
58
+ command: sh -c "npm run build && node --no-warnings dist/index.js"
ui/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
ui/Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WEB Dockerfile
2
+ # Development Stage
3
+ FROM node:25-slim AS dev
4
+ WORKDIR /app
5
+ COPY package*.json ./
6
+ RUN npm install
7
+ COPY . .
8
+ EXPOSE 5173
9
+ CMD ["npm", "run", "dev", "--", "--host"]
10
+
11
+ # Production Stage
12
+ FROM node:25-slim AS build
13
+ WORKDIR /app
14
+ COPY package*.json ./
15
+ RUN npm install
16
+ COPY . .
17
+ RUN npm run build
18
+
19
+ FROM nginx:stable-alpine AS prod
20
+ COPY --from=build /app/dist /usr/share/nginx/html
21
+ EXPOSE 80
22
+ CMD ["nginx", "-g", "daemon off;"]
ui/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
ui/eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
ui/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ui</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
ui/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
ui/package.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ui",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^19.2.4",
14
+ "react-dom": "^19.2.4",
15
+ "axios": "^1.8.1",
16
+ "lucide-react": "^0.477.0"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.39.4",
20
+ "@types/node": "^24.12.0",
21
+ "@types/react": "^19.2.14",
22
+ "@types/react-dom": "^19.2.3",
23
+ "@vitejs/plugin-react": "^6.0.1",
24
+ "@tailwindcss/postcss": "^4.0.0",
25
+ "autoprefixer": "^10.4.20",
26
+ "eslint": "^9.39.4",
27
+ "eslint-plugin-react-hooks": "^7.0.1",
28
+ "eslint-plugin-react-refresh": "^0.5.2",
29
+ "globals": "^17.4.0",
30
+ "postcss": "^8.5.3",
31
+ "tailwindcss": "^4.0.9",
32
+ "typescript": "~5.9.3",
33
+ "typescript-eslint": "^8.57.0",
34
+ "vite": "^8.0.1"
35
+ }
36
+ }
ui/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ autoprefixer: {},
5
+ },
6
+ }
ui/public/favicon.svg ADDED
ui/public/icons.svg ADDED
ui/src/App.css ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .counter {
2
+ font-size: 16px;
3
+ padding: 5px 10px;
4
+ border-radius: 5px;
5
+ color: var(--accent);
6
+ background: var(--accent-bg);
7
+ border: 2px solid transparent;
8
+ transition: border-color 0.3s;
9
+ margin-bottom: 24px;
10
+
11
+ &:hover {
12
+ border-color: var(--accent-border);
13
+ }
14
+ &:focus-visible {
15
+ outline: 2px solid var(--accent);
16
+ outline-offset: 2px;
17
+ }
18
+ }
19
+
20
+ .hero {
21
+ position: relative;
22
+
23
+ .base,
24
+ .framework,
25
+ .vite {
26
+ inset-inline: 0;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .base {
31
+ width: 170px;
32
+ position: relative;
33
+ z-index: 0;
34
+ }
35
+
36
+ .framework,
37
+ .vite {
38
+ position: absolute;
39
+ }
40
+
41
+ .framework {
42
+ z-index: 1;
43
+ top: 34px;
44
+ height: 28px;
45
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
+ scale(1.4);
47
+ }
48
+
49
+ .vite {
50
+ z-index: 0;
51
+ top: 107px;
52
+ height: 26px;
53
+ width: auto;
54
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
+ scale(0.8);
56
+ }
57
+ }
58
+
59
+ #center {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 25px;
63
+ place-content: center;
64
+ place-items: center;
65
+ flex-grow: 1;
66
+
67
+ @media (max-width: 1024px) {
68
+ padding: 32px 20px 24px;
69
+ gap: 18px;
70
+ }
71
+ }
72
+
73
+ #next-steps {
74
+ display: flex;
75
+ border-top: 1px solid var(--border);
76
+ text-align: left;
77
+
78
+ & > div {
79
+ flex: 1 1 0;
80
+ padding: 32px;
81
+ @media (max-width: 1024px) {
82
+ padding: 24px 20px;
83
+ }
84
+ }
85
+
86
+ .icon {
87
+ margin-bottom: 16px;
88
+ width: 22px;
89
+ height: 22px;
90
+ }
91
+
92
+ @media (max-width: 1024px) {
93
+ flex-direction: column;
94
+ text-align: center;
95
+ }
96
+ }
97
+
98
+ #docs {
99
+ border-right: 1px solid var(--border);
100
+
101
+ @media (max-width: 1024px) {
102
+ border-right: none;
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+ }
106
+
107
+ #next-steps ul {
108
+ list-style: none;
109
+ padding: 0;
110
+ display: flex;
111
+ gap: 8px;
112
+ margin: 32px 0 0;
113
+
114
+ .logo {
115
+ height: 18px;
116
+ }
117
+
118
+ a {
119
+ color: var(--text-h);
120
+ font-size: 16px;
121
+ border-radius: 6px;
122
+ background: var(--social-bg);
123
+ display: flex;
124
+ padding: 6px 12px;
125
+ align-items: center;
126
+ gap: 8px;
127
+ text-decoration: none;
128
+ transition: box-shadow 0.3s;
129
+
130
+ &:hover {
131
+ box-shadow: var(--shadow);
132
+ }
133
+ .button-icon {
134
+ height: 18px;
135
+ width: 18px;
136
+ }
137
+ }
138
+
139
+ @media (max-width: 1024px) {
140
+ margin-top: 20px;
141
+ flex-wrap: wrap;
142
+ justify-content: center;
143
+
144
+ li {
145
+ flex: 1 1 calc(50% - 8px);
146
+ }
147
+
148
+ a {
149
+ width: 100%;
150
+ justify-content: center;
151
+ box-sizing: border-box;
152
+ }
153
+ }
154
+ }
155
+
156
+ #spacer {
157
+ height: 88px;
158
+ border-top: 1px solid var(--border);
159
+ @media (max-width: 1024px) {
160
+ height: 48px;
161
+ }
162
+ }
163
+
164
+ .ticks {
165
+ position: relative;
166
+ width: 100%;
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: -4.5px;
173
+ border: 5px solid transparent;
174
+ }
175
+
176
+ &::before {
177
+ left: 0;
178
+ border-left-color: var(--border);
179
+ }
180
+ &::after {
181
+ right: 0;
182
+ border-right-color: var(--border);
183
+ }
184
+ }
ui/src/App.tsx ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import axios from 'axios';
3
+ import {
4
+ Shield,
5
+ Eye,
6
+ Lock,
7
+ RefreshCw,
8
+ AlertCircle,
9
+ CheckCircle2,
10
+ Copy,
11
+ ChevronRight,
12
+ Database,
13
+ ArrowRightLeft,
14
+ Languages,
15
+ BookOpen,
16
+ X,
17
+ Code2
18
+ } from 'lucide-react';
19
+
20
+ interface Entity {
21
+ entity_type: string;
22
+ start: number;
23
+ end: number;
24
+ score: number;
25
+ }
26
+
27
+ interface RedactResponse {
28
+ original_text: string;
29
+ redacted_text: string;
30
+ detected_language: string;
31
+ detected_entities: Entity[];
32
+ }
33
+
34
+ function App() {
35
+ const [text, setText] = useState('');
36
+ const [language, setLanguage] = useState('auto');
37
+ const [result, setResult] = useState<RedactResponse | null>(null);
38
+ const [loading, setLoading] = useState(false);
39
+ const [error, setError] = useState<string | null>(null);
40
+ const [apiStatus, setApiStatus] = useState<'checking' | 'online' | 'offline'>('checking');
41
+ const [copied, setCopied] = useState(false);
42
+ const [showDocs, setShowDocs] = useState(false);
43
+
44
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
45
+
46
+ useEffect(() => {
47
+ const checkStatus = async () => {
48
+ try {
49
+ await axios.get(`${API_URL}/`);
50
+ setApiStatus('online');
51
+ } catch (err) {
52
+ setApiStatus('offline');
53
+ }
54
+ };
55
+ checkStatus();
56
+ }, [API_URL]);
57
+
58
+ const handleRedact = async () => {
59
+ if (!text.trim()) return;
60
+ setLoading(true);
61
+ setError(null);
62
+ try {
63
+ const response = await axios.post(`${API_URL}/redact`, {
64
+ text,
65
+ language
66
+ });
67
+ setResult(response.data);
68
+ } catch (err: any) {
69
+ setError(err.response?.data?.detail || "Failed to connect to the PII Moderator API.");
70
+ } finally {
71
+ setLoading(false);
72
+ }
73
+ };
74
+
75
+ const handleCopy = () => {
76
+ if (result) {
77
+ navigator.clipboard.writeText(result.redacted_text);
78
+ setCopied(true);
79
+ setTimeout(() => setCopied(false), 2000);
80
+ }
81
+ };
82
+
83
+ const entityColors: Record<string, string> = {
84
+ PERSON: 'bg-indigo-100 text-indigo-700 border-indigo-200',
85
+ EMAIL_ADDRESS: 'bg-emerald-100 text-emerald-700 border-emerald-200',
86
+ PHONE_NUMBER: 'bg-amber-100 text-amber-700 border-amber-200',
87
+ LOCATION: 'bg-rose-100 text-rose-700 border-rose-200',
88
+ URL: 'bg-sky-100 text-sky-700 border-sky-200',
89
+ DEFAULT: 'bg-slate-100 text-slate-700 border-slate-200'
90
+ };
91
+
92
+ return (
93
+ <div className="min-h-screen bg-[#f8fafc] text-slate-900 selection:bg-blue-100 transition-all duration-500">
94
+
95
+ {/* Documentation Sidebar */}
96
+ <div className={`fixed top-0 right-0 h-full w-full md:w-[500px] bg-white shadow-2xl z-50 transform transition-transform duration-500 ease-in-out border-l border-slate-200 flex flex-col ${showDocs ? 'translate-x-0' : 'translate-x-full'}`}>
97
+ <div className="p-8 border-b border-slate-100 flex items-center justify-between">
98
+ <div className="flex items-center gap-3">
99
+ <BookOpen className="text-blue-600 w-6 h-6" />
100
+ <h2 className="text-xl font-black tracking-tight uppercase tracking-[0.1em]">Documentation</h2>
101
+ </div>
102
+ <button onClick={() => setShowDocs(false)} className="p-2 hover:bg-slate-100 rounded-xl text-slate-400 transition-colors">
103
+ <X className="w-6 h-6" />
104
+ </button>
105
+ </div>
106
+ <div className="p-8 overflow-y-auto flex-grow prose prose-slate max-w-none">
107
+ <section className="mb-10">
108
+ <h3 className="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2"><Code2 className="w-5 h-5 text-blue-500" /> API Integration</h3>
109
+ <p className="text-slate-600 text-sm leading-relaxed">To integrate the PII Moderator into your existing backend, use our REST API:</p>
110
+ <div className="bg-slate-900 rounded-xl p-4 mt-4 font-mono text-[11px] text-emerald-400">
111
+ <span className="text-pink-400 italic">POST</span> /redact<br/>
112
+ {`{ "text": "Bonjour, je m'appelle Alice", "language": "fr" }`}
113
+ </div>
114
+ </section>
115
+
116
+ <section className="mb-10">
117
+ <h3 className="text-lg font-bold text-slate-900 mb-4">Supported Languages</h3>
118
+ <div className="grid grid-cols-2 gap-4">
119
+ <div className="p-4 bg-slate-50 border border-slate-200 rounded-xl">
120
+ <span className="font-bold block text-sm">English (en)</span>
121
+ <span className="text-[10px] text-slate-400">Optimized with en_core_web_lg</span>
122
+ </div>
123
+ <div className="p-4 bg-slate-50 border border-slate-200 rounded-xl">
124
+ <span className="font-bold block text-sm">French (fr)</span>
125
+ <span className="text-[10px] text-slate-400">Optimized with fr_core_news_lg</span>
126
+ </div>
127
+ </div>
128
+ </section>
129
+
130
+ <section className="mb-10">
131
+ <h3 className="text-lg font-bold text-slate-900 mb-4 underline decoration-blue-200 underline-offset-8">How it works</h3>
132
+ <ol className="text-sm text-slate-600 space-y-4 list-decimal pl-4">
133
+ <li><strong>Natural Language Processing:</strong> We use spaCy's large models to identify linguistic patterns.</li>
134
+ <li><strong>Named Entity Recognition (NER):</strong> The analyzer engine extracts PII like names, addresses, and credit cards.</li>
135
+ <li><strong>Placeholder Anonymization:</strong> Detected entities are replaced by standardized placeholders to preserve the context of the sentence for LLM usage.</li>
136
+ </ol>
137
+ </section>
138
+ </div>
139
+ </div>
140
+
141
+ <div className="fixed inset-0 overflow-hidden -z-10">
142
+ <div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] rounded-full bg-blue-100/50 blur-[120px]" />
143
+ <div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] rounded-full bg-indigo-100/50 blur-[120px]" />
144
+ </div>
145
+
146
+ <div className="max-w-7xl mx-auto px-6 py-12 lg:px-8">
147
+ <header className="flex flex-col md:flex-row md:items-center justify-between mb-16 gap-6">
148
+ <div className="flex items-center space-x-4">
149
+ <div className="relative">
150
+ <div className="absolute inset-0 bg-blue-600 blur-lg opacity-30 animate-pulse" />
151
+ <div className="relative bg-white p-3 rounded-2xl shadow-xl border border-slate-100">
152
+ <Shield className="text-blue-600 w-8 h-8" strokeWidth={2.5} />
153
+ </div>
154
+ </div>
155
+ <div>
156
+ <h1 className="text-3xl font-black tracking-tight text-slate-900 flex items-center gap-2">
157
+ Privacy Gateway <span className="text-blue-600">v1.1</span>
158
+ </h1>
159
+ <div className="flex items-center space-x-2 mt-1">
160
+ <span className={`w-2 h-2 rounded-full ${apiStatus === 'online' ? 'bg-emerald-500' : 'bg-rose-500'}`} />
161
+ <span className="text-[11px] font-bold uppercase tracking-widest text-slate-400">
162
+ {apiStatus === 'online' ? 'Multi-Language Support Active' : 'Offline'}
163
+ </span>
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <nav className="flex items-center space-x-1 p-1 bg-slate-100 rounded-xl border border-slate-200/50 shadow-inner">
169
+ <button
170
+ onClick={() => setShowDocs(true)}
171
+ className="px-4 py-2 text-slate-500 font-bold rounded-lg text-sm hover:text-slate-900 transition-colors flex items-center gap-2"
172
+ >
173
+ <BookOpen className="w-4 h-4" /> Documentation
174
+ </button>
175
+ <div className="w-px h-4 bg-slate-300 mx-2" />
176
+ <div className="flex items-center bg-white rounded-lg px-2 py-1 shadow-sm border border-slate-200/50">
177
+ <Languages className="w-4 h-4 text-blue-500 mr-2" />
178
+ <select
179
+ value={language}
180
+ onChange={(e) => setLanguage(e.target.value)}
181
+ className="bg-transparent border-none outline-none text-xs font-black uppercase tracking-wider text-slate-700 cursor-pointer"
182
+ >
183
+ <option value="auto">Auto-detect</option>
184
+ <option value="en">English (NER-lg)</option>
185
+ <option value="fr">French (NER-lg)</option>
186
+ </select>
187
+ </div>
188
+ </nav>
189
+ </header>
190
+
191
+ {error && (
192
+ <div className="mb-8 p-4 bg-white border border-rose-200 rounded-2xl shadow-sm flex items-start space-x-4 animate-in slide-in-from-top-4">
193
+ <div className="bg-rose-50 p-2 rounded-xl text-rose-600"><AlertCircle className="w-6 h-6" /></div>
194
+ <div><h3 className="text-sm font-bold text-rose-800 uppercase tracking-wider">Error</h3><p className="text-sm text-rose-600 mt-1">{error}</p></div>
195
+ </div>
196
+ )}
197
+
198
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-10 items-start">
199
+ <div className="lg:col-span-5 space-y-6">
200
+ <div className="group relative">
201
+ <div className="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-[2rem] blur opacity-10 group-focus-within:opacity-20 transition duration-500" />
202
+ <div className="relative bg-white rounded-[2rem] shadow-xl border border-slate-200 p-8">
203
+ <div className="flex items-center justify-between mb-6">
204
+ <div className="flex items-center gap-2">
205
+ <Eye className="w-4 h-4 text-slate-400" />
206
+ <span className="text-xs font-black uppercase tracking-widest text-slate-400">Input Text</span>
207
+ </div>
208
+ <Database className="w-4 h-4 text-slate-300" />
209
+ </div>
210
+ <textarea
211
+ className="w-full h-80 bg-transparent text-slate-700 font-medium leading-relaxed placeholder-slate-300 outline-none resize-none"
212
+ placeholder={language === 'fr' ? "Collez votre texte ici... ex: 'Bonjour, je m'appelle Alice'" : "Paste text here... ex: 'Hello, my name is Alice'"}
213
+ value={text}
214
+ onChange={(e) => setText(e.target.value)}
215
+ />
216
+ <div className="mt-8 pt-8 border-t border-slate-50">
217
+ <button
218
+ onClick={handleRedact}
219
+ disabled={loading || apiStatus === 'offline'}
220
+ className={`group relative w-full py-4 rounded-2xl font-black text-sm uppercase tracking-widest text-white transition-all ${loading || apiStatus === 'offline' ? 'bg-slate-300' : 'bg-slate-900 hover:shadow-2xl hover:-translate-y-1'}`}
221
+ >
222
+ <span className="relative flex items-center justify-center gap-3">
223
+ {loading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <><ArrowRightLeft className="w-5 h-5" /><span>Redact for LLM</span></>}
224
+ </span>
225
+ </button>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ <div className="hidden lg:flex lg:col-span-1 h-full items-center justify-center">
232
+ <div className="w-px h-64 bg-slate-200 relative"><div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-[#f8fafc] p-2"><CheckCircle2 className={`w-5 h-5 ${result ? 'text-emerald-500' : 'text-slate-300'}`} /></div></div>
233
+ </div>
234
+
235
+ <div className="lg:col-span-6 space-y-6">
236
+ <div className="bg-slate-900 rounded-[2rem] shadow-2xl p-8 min-h-[500px] flex flex-col border border-slate-800 relative overflow-hidden">
237
+ <div className="absolute inset-0 opacity-[0.03] pointer-events-none" style={{ backgroundImage: 'radial-gradient(#ffffff 1px, transparent 1px)', backgroundSize: '32px 32px' }} />
238
+ <div className="flex items-center justify-between mb-8 relative z-10">
239
+ <div className="flex items-center gap-2"><Lock className="w-4 h-4 text-emerald-500" /><span className="text-[10px] font-black uppercase tracking-[0.2em] text-emerald-500/80">Scrubbed Output ({result ? result.detected_language : language})</span></div>
240
+ {result && <button onClick={handleCopy} className="text-[10px] font-black uppercase tracking-widest px-3 py-1.5 bg-white/5 border border-white/10 rounded-lg text-white hover:bg-white/10">{copied ? 'Copied' : 'Copy'}</button>}
241
+ </div>
242
+ <div className="flex-grow relative z-10">
243
+ {!result ? (
244
+ <div className="h-full flex flex-col items-center justify-center text-center p-8 space-y-4 text-slate-500 font-medium italic"><Lock className="w-8 h-8 opacity-20" /><p>Sanitize your prompt to view results...</p></div>
245
+ ) : <div className="text-emerald-500 font-mono text-sm whitespace-pre-wrap animate-in fade-in">{result.redacted_text}</div>}
246
+ </div>
247
+ {result && result.detected_entities.length > 0 && (
248
+ <div className="mt-8 pt-8 border-t border-white/5 relative z-10">
249
+ <h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-4">Metadata Analysis</h4>
250
+ <div className="flex flex-wrap gap-2">
251
+ {result.detected_entities.map((ent, idx) => {
252
+ const style = entityColors[ent.entity_type] || entityColors.DEFAULT;
253
+ return (<div key={idx} className={`px-3 py-1.5 rounded-xl border text-[10px] font-black uppercase tracking-wider flex items-center gap-2 ${style}`}>{ent.entity_type} <span className="opacity-50 text-[9px]">{Math.round(ent.score * 100)}%</span></div>);
254
+ })}
255
+ </div>
256
+ </div>
257
+ )}
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ export default App;
ui/src/assets/hero.png ADDED
ui/src/assets/react.svg ADDED
ui/src/assets/vite.svg ADDED
ui/src/index.css ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ --color-brand: #3b82f6;
5
+ --color-brand-dark: #1d4ed8;
6
+ --radius-xl: 1rem;
7
+ --radius-2xl: 1.5rem;
8
+ }
9
+
10
+ body {
11
+ @apply bg-slate-50 text-slate-900 antialiased;
12
+ }
13
+
14
+ ::-webkit-scrollbar {
15
+ width: 8px;
16
+ }
17
+
18
+ ::-webkit-scrollbar-track {
19
+ @apply bg-transparent;
20
+ }
21
+
22
+ ::-webkit-scrollbar-thumb {
23
+ @apply bg-slate-200 rounded-full hover:bg-slate-300 transition-colors;
24
+ }
ui/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
ui/tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
ui/tsconfig.app.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }
ui/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
ui/tsconfig.node.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
ui/vite.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })