knowledge-rag 3.3.0__tar.gz → 3.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: knowledge-rag
3
- Version: 3.3.0
3
+ Version: 3.3.2
4
4
  Summary: Local RAG System for Claude Code — Hybrid search + Cross-encoder Reranking + 12 MCP Tools. Zero external servers.
5
5
  Project-URL: Homepage, https://github.com/lyonzin/knowledge-rag
6
6
  Project-URL: Repository, https://github.com/lyonzin/knowledge-rag
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  <div align="center">
40
40
 
41
- ![Version](https://img.shields.io/badge/version-3.3.0-blue.svg)
41
+ ![Version](https://img.shields.io/badge/version-3.3.2-blue.svg)
42
42
  ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-green.svg)
43
43
  ![License](https://img.shields.io/badge/license-MIT-yellow.svg)
44
44
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)
@@ -408,21 +408,23 @@ flowchart LR
408
408
 
409
409
  ### Quick Start (3 steps)
410
410
 
411
- **Step 1: Clone and install**
411
+ **Step 1: Install**
412
412
 
413
413
  ```bash
414
- # Clone to your home directory
414
+ # Option A: pip install (recommended)
415
+ pip install knowledge-rag
416
+ knowledge-rag init # Exports config template, presets, creates documents/
417
+
418
+ # Option B: Clone from source
415
419
  git clone https://github.com/lyonzin/knowledge-rag.git ~/knowledge-rag
416
420
  cd ~/knowledge-rag
417
-
418
- # Create virtual environment and install
419
421
  python3 -m venv venv
420
422
  source venv/bin/activate # Linux/macOS
421
423
  # .\venv\Scripts\activate # Windows
422
424
  pip install -r requirements.txt
423
425
  ```
424
426
 
425
- > **Windows users**: `~/knowledge-rag` becomes `C:\Users\YourName\knowledge-rag`. Use `python` instead of `python3`.
427
+ > **Windows users**: Use `python` instead of `python3`.
426
428
 
427
429
  **Step 2: Configure Claude Code**
428
430
 
@@ -1161,7 +1163,7 @@ pip install rank-bm25
1161
1163
 
1162
1164
  ### "ModuleNotFoundError: No module named 'mcp_server'"
1163
1165
 
1164
- This occurs when Claude Code doesn't set the working directory correctly. Use the `cmd /c "cd /d ... && python"` wrapper in your MCP config (see [Installation](#configure-mcp-for-claude-code)).
1166
+ This occurs when Claude Code doesn't set the working directory correctly. Use the `cmd /c "cd /d ... && python"` wrapper in your MCP config (see [Installation](#installation)).
1165
1167
 
1166
1168
  ### Dimension mismatch after upgrade
1167
1169
 
@@ -1193,6 +1195,21 @@ With ~200 documents, expect ~300-500MB RAM. The embedding model (~50MB) and rera
1193
1195
 
1194
1196
  ## Changelog
1195
1197
 
1198
+ ### v3.3.2 (2026-04-06)
1199
+
1200
+ - **FIX**: Full type validation on all YAML config values — wrong types warn and fall back to defaults
1201
+ - **FIX**: Bounds validation for chunk_size, chunk_overlap, default_results, max_results, embedding_dim
1202
+ - **FIX**: `keyword_routes` with string values instead of lists detected and removed with warning
1203
+ - **FIX**: `reranker_enabled: "yes"` (string) corrected to boolean with warning
1204
+ - **FIX**: Synced version strings across all source files
1205
+ - **FIX**: Broken README anchor, duplicate keyword, error handling in `knowledge-rag init`
1206
+
1207
+ ### v3.3.1 (2026-04-06)
1208
+
1209
+ - **FIX**: YAML null values (`category_mappings:` without value) no longer crash the server — falls back to defaults
1210
+ - **FIX**: Presets and config template now bundled in pip wheel (`knowledge-rag init` exports them)
1211
+ - **NEW**: `knowledge-rag init` CLI command — exports config template, presets, and creates documents/ in current directory
1212
+
1196
1213
  ### v3.3.0 (2026-04-05)
1197
1214
 
1198
1215
  - **NEW**: YAML configuration system — fully customizable via `config.yaml`
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div align="center">
4
4
 
5
- ![Version](https://img.shields.io/badge/version-3.3.0-blue.svg)
5
+ ![Version](https://img.shields.io/badge/version-3.3.2-blue.svg)
6
6
  ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-green.svg)
7
7
  ![License](https://img.shields.io/badge/license-MIT-yellow.svg)
8
8
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)
@@ -372,21 +372,23 @@ flowchart LR
372
372
 
373
373
  ### Quick Start (3 steps)
374
374
 
375
- **Step 1: Clone and install**
375
+ **Step 1: Install**
376
376
 
377
377
  ```bash
378
- # Clone to your home directory
378
+ # Option A: pip install (recommended)
379
+ pip install knowledge-rag
380
+ knowledge-rag init # Exports config template, presets, creates documents/
381
+
382
+ # Option B: Clone from source
379
383
  git clone https://github.com/lyonzin/knowledge-rag.git ~/knowledge-rag
380
384
  cd ~/knowledge-rag
381
-
382
- # Create virtual environment and install
383
385
  python3 -m venv venv
384
386
  source venv/bin/activate # Linux/macOS
385
387
  # .\venv\Scripts\activate # Windows
386
388
  pip install -r requirements.txt
387
389
  ```
388
390
 
389
- > **Windows users**: `~/knowledge-rag` becomes `C:\Users\YourName\knowledge-rag`. Use `python` instead of `python3`.
391
+ > **Windows users**: Use `python` instead of `python3`.
390
392
 
391
393
  **Step 2: Configure Claude Code**
392
394
 
@@ -1125,7 +1127,7 @@ pip install rank-bm25
1125
1127
 
1126
1128
  ### "ModuleNotFoundError: No module named 'mcp_server'"
1127
1129
 
1128
- This occurs when Claude Code doesn't set the working directory correctly. Use the `cmd /c "cd /d ... && python"` wrapper in your MCP config (see [Installation](#configure-mcp-for-claude-code)).
1130
+ This occurs when Claude Code doesn't set the working directory correctly. Use the `cmd /c "cd /d ... && python"` wrapper in your MCP config (see [Installation](#installation)).
1129
1131
 
1130
1132
  ### Dimension mismatch after upgrade
1131
1133
 
@@ -1157,6 +1159,21 @@ With ~200 documents, expect ~300-500MB RAM. The embedding model (~50MB) and rera
1157
1159
 
1158
1160
  ## Changelog
1159
1161
 
1162
+ ### v3.3.2 (2026-04-06)
1163
+
1164
+ - **FIX**: Full type validation on all YAML config values — wrong types warn and fall back to defaults
1165
+ - **FIX**: Bounds validation for chunk_size, chunk_overlap, default_results, max_results, embedding_dim
1166
+ - **FIX**: `keyword_routes` with string values instead of lists detected and removed with warning
1167
+ - **FIX**: `reranker_enabled: "yes"` (string) corrected to boolean with warning
1168
+ - **FIX**: Synced version strings across all source files
1169
+ - **FIX**: Broken README anchor, duplicate keyword, error handling in `knowledge-rag init`
1170
+
1171
+ ### v3.3.1 (2026-04-06)
1172
+
1173
+ - **FIX**: YAML null values (`category_mappings:` without value) no longer crash the server — falls back to defaults
1174
+ - **FIX**: Presets and config template now bundled in pip wheel (`knowledge-rag init` exports them)
1175
+ - **NEW**: `knowledge-rag init` CLI command — exports config template, presets, and creates documents/ in current directory
1176
+
1160
1177
  ### v3.3.0 (2026-04-05)
1161
1178
 
1162
1179
  - **NEW**: YAML configuration system — fully customizable via `config.yaml`
@@ -1,6 +1,6 @@
1
1
  """Knowledge RAG MCP Server - Local Retrieval-Augmented Generation System"""
2
2
 
3
- __version__ = "3.2.3"
3
+ __version__ = "3.3.2"
4
4
  __author__ = "Ailton Rocha (Lyon.)"
5
5
 
6
6
  from .config import Config
@@ -0,0 +1,487 @@
1
+ """Configuration for Knowledge RAG System v3.3.2 — YAML-configurable"""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Dict, List
7
+
8
+ import yaml
9
+
10
+ # ============================================================================
11
+ # BASE DIRECTORY RESOLUTION
12
+ # ============================================================================
13
+ # Priority: 1. KNOWLEDGE_RAG_DIR env var 2. Source checkout 3. CWD
14
+
15
+ _source_dir = Path(__file__).parent.parent
16
+
17
+
18
+ _SUPPORTED_SUFFIXES = frozenset([".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv"])
19
+
20
+
21
+ def _has_documents(path: Path) -> bool:
22
+ """Check if path has a documents/ dir with actual supported files (follows symlinks)."""
23
+ docs_dir = path / "documents"
24
+ if not docs_dir.exists():
25
+ return False
26
+ for root, _, files in os.walk(docs_dir, followlinks=True):
27
+ for f in files:
28
+ if Path(f).suffix.lower() in _SUPPORTED_SUFFIXES:
29
+ return True
30
+ return False
31
+
32
+
33
+ if os.environ.get("KNOWLEDGE_RAG_DIR"):
34
+ BASE_DIR = Path(os.environ["KNOWLEDGE_RAG_DIR"])
35
+ elif _has_documents(_source_dir):
36
+ BASE_DIR = _source_dir
37
+ elif _has_documents(Path.cwd()):
38
+ BASE_DIR = Path.cwd()
39
+ else:
40
+ BASE_DIR = Path.cwd()
41
+
42
+
43
+ # ============================================================================
44
+ # YAML CONFIG LOADER
45
+ # ============================================================================
46
+
47
+
48
+ def _load_yaml_config() -> dict:
49
+ """Load config.yaml from BASE_DIR if it exists, otherwise return empty dict."""
50
+ config_path = BASE_DIR / "config.yaml"
51
+ if not config_path.exists():
52
+ return {}
53
+
54
+ try:
55
+ with open(config_path, "r", encoding="utf-8") as f:
56
+ data = yaml.safe_load(f)
57
+ if not isinstance(data, dict):
58
+ print("[WARN] config.yaml is not a valid mapping, ignoring")
59
+ return {}
60
+ print(f"[INFO] Loaded config from {config_path}")
61
+ return data
62
+ except yaml.YAMLError as e:
63
+ print(f"[WARN] Failed to parse config.yaml: {e} — using defaults")
64
+ return {}
65
+
66
+
67
+ _yaml = _load_yaml_config()
68
+
69
+
70
+ def _get(section: str, key: str, default):
71
+ """Get a value from the YAML config with section.key path, falling back to default."""
72
+ s = _yaml.get(section, {})
73
+ if not isinstance(s, dict):
74
+ return default
75
+ val = s.get(key)
76
+ if val is None:
77
+ return default
78
+ # Skip type check when default is None (caller handles validation)
79
+ if default is None:
80
+ return val
81
+ # YAML parses "yes"/"no" as bool, but explicit string "yes" stays str
82
+ if not isinstance(val, type(default)):
83
+ print(
84
+ f"[WARN] config.yaml: {section}.{key} has wrong type "
85
+ f"(expected {type(default).__name__}, got {type(val).__name__}), using default"
86
+ )
87
+ return default
88
+ return val
89
+
90
+
91
+ def _get_top(key: str, default):
92
+ """Get a top-level value from YAML, falling back to default if missing or None."""
93
+ val = _yaml.get(key)
94
+ if val is None:
95
+ return default
96
+ if not isinstance(val, dict):
97
+ print(f"[WARN] config.yaml: {key} has wrong type (expected dict, got {type(val).__name__}), using default")
98
+ return default
99
+ return val
100
+
101
+
102
+ # ============================================================================
103
+ # DEFAULTS (used when no config.yaml or field is omitted)
104
+ # ============================================================================
105
+
106
+ _DEFAULT_CATEGORY_MAPPINGS = {
107
+ "security/redteam": "redteam",
108
+ "security/blueteam": "blueteam",
109
+ "security/ctf": "ctf",
110
+ "security": "security",
111
+ "aar": "aar",
112
+ "logscale": "logscale",
113
+ "development": "development",
114
+ "general": "general",
115
+ }
116
+
117
+ _DEFAULT_KEYWORD_ROUTES = {
118
+ "logscale": [
119
+ "logscale",
120
+ "lql",
121
+ "cql",
122
+ "humio",
123
+ "crowdstrike query",
124
+ "formattime",
125
+ "groupby",
126
+ "base64decode",
127
+ "case{}",
128
+ "regex",
129
+ ],
130
+ "redteam": [
131
+ "pentest",
132
+ "exploit",
133
+ "payload",
134
+ "reverse shell",
135
+ "privilege escalation",
136
+ "lateral movement",
137
+ "c2",
138
+ "beacon",
139
+ "cobalt strike",
140
+ "metasploit",
141
+ "gtfobins",
142
+ "lolbas",
143
+ "lolbin",
144
+ "suid",
145
+ "sudo",
146
+ "byovd",
147
+ "lol driver",
148
+ "lolad",
149
+ "lolapps",
150
+ "hacktricks",
151
+ "privesc",
152
+ "kerberoast",
153
+ "dcsync",
154
+ "golden ticket",
155
+ "pass-the-hash",
156
+ "bloodhound",
157
+ "mimikatz",
158
+ "rubeus",
159
+ "certipy",
160
+ "adcs",
161
+ "sqli",
162
+ "xss",
163
+ "ssti",
164
+ "ssrf",
165
+ "lfi",
166
+ "rfi",
167
+ "xxe",
168
+ "deserialization",
169
+ "ysoserial",
170
+ "upload bypass",
171
+ "web shell",
172
+ "hash cracking",
173
+ "hashcat",
174
+ "waf bypass",
175
+ "amsi bypass",
176
+ "uac bypass",
177
+ "potato",
178
+ "searchsploit",
179
+ "exploit-db",
180
+ "cve",
181
+ ],
182
+ "blueteam": [
183
+ "detection",
184
+ "sigma",
185
+ "yara",
186
+ "ioc",
187
+ "threat hunting",
188
+ "incident response",
189
+ "forensics",
190
+ "malware analysis",
191
+ ],
192
+ "ctf": [
193
+ "ctf",
194
+ "flag",
195
+ "hackthebox",
196
+ "htb",
197
+ "tryhackme",
198
+ "picoctf",
199
+ "writeup",
200
+ "challenge",
201
+ ],
202
+ "development": [
203
+ "python",
204
+ "typescript",
205
+ "javascript",
206
+ "api",
207
+ "fastapi",
208
+ "django",
209
+ "react",
210
+ "nodejs",
211
+ ],
212
+ "security": [
213
+ "anti-bot",
214
+ "antibot",
215
+ "js challenge",
216
+ "javascript challenge",
217
+ "cdp detection",
218
+ "runtime.enable",
219
+ "puppeteer",
220
+ "playwright",
221
+ "selenium",
222
+ "nodriver",
223
+ "stealth",
224
+ "undetected",
225
+ "ja3",
226
+ "ja4",
227
+ "tls fingerprint",
228
+ "fingerprinting",
229
+ "curl_cffi",
230
+ "got-scraping",
231
+ "impersonate",
232
+ "http/2 settings",
233
+ "browser fingerprint",
234
+ "canvas fingerprint",
235
+ "webgl fingerprint",
236
+ "navigator.webdriver",
237
+ "audio context",
238
+ "hardware concurrency",
239
+ "waf bypass",
240
+ "aws waf",
241
+ "cloudflare bypass",
242
+ "akamai bypass",
243
+ "datadome",
244
+ "perimeterx",
245
+ "imperva bypass",
246
+ "8kb bypass",
247
+ "body size limit",
248
+ "json sqli",
249
+ "behavioral",
250
+ "mouse movement",
251
+ "ghost-cursor",
252
+ "humanized",
253
+ "flaresolverr",
254
+ "turnstile",
255
+ "rebrowser",
256
+ "botbrowser",
257
+ ],
258
+ }
259
+
260
+ _DEFAULT_QUERY_EXPANSIONS = {
261
+ "sqli": ["sql injection", "sqli"],
262
+ "sql injection": ["sql injection", "sqli"],
263
+ "xss": ["cross-site scripting", "xss"],
264
+ "cross-site scripting": ["cross-site scripting", "xss"],
265
+ "ssrf": ["server-side request forgery", "ssrf"],
266
+ "lfi": ["local file inclusion", "lfi"],
267
+ "rfi": ["remote file inclusion", "rfi"],
268
+ "rce": ["remote code execution", "rce"],
269
+ "xxe": ["xml external entity", "xxe"],
270
+ "ssti": ["server-side template injection", "ssti"],
271
+ "idor": ["insecure direct object reference", "idor"],
272
+ "csrf": ["cross-site request forgery", "csrf"],
273
+ "privesc": ["privilege escalation", "privesc"],
274
+ "priv esc": ["privilege escalation", "privesc"],
275
+ "privilege escalation": ["privilege escalation", "privesc"],
276
+ "deserialization": ["deserialization", "deserialisation", "insecure deserialization"],
277
+ "pth": ["pass-the-hash", "pth"],
278
+ "pass-the-hash": ["pass-the-hash", "pth"],
279
+ "dcsync": ["dcsync", "dc sync", "domain controller sync"],
280
+ "kerberoast": ["kerberoasting", "kerberoast"],
281
+ "kerberoasting": ["kerberoasting", "kerberoast"],
282
+ "asrep": ["as-rep roasting", "asrep", "asreproast"],
283
+ "bloodhound": ["bloodhound", "sharphound"],
284
+ "mimikatz": ["mimikatz", "sekurlsa", "logonpasswords"],
285
+ "hashcat": ["hashcat", "hash cracking", "hash crack"],
286
+ "john": ["john the ripper", "john", "jtr"],
287
+ "revshell": ["reverse shell", "revshell", "rev shell"],
288
+ "reverse shell": ["reverse shell", "revshell"],
289
+ "webshell": ["web shell", "webshell"],
290
+ "web shell": ["web shell", "webshell"],
291
+ "waf": ["web application firewall", "waf"],
292
+ "amsi": ["antimalware scan interface", "amsi", "amsi bypass"],
293
+ "uac": ["user account control", "uac", "uac bypass"],
294
+ "potato": ["potato", "juicypotato", "sweetpotato", "godpotato", "efspotato", "printspoofer"],
295
+ "ntlm": ["ntlm", "net-ntlmv2", "ntlmv2"],
296
+ "smb": ["smb", "server message block", "samba"],
297
+ "ldap": ["ldap", "lightweight directory access protocol"],
298
+ "ad": ["active directory", "ad"],
299
+ "active directory": ["active directory", "ad"],
300
+ "defender": ["windows defender", "defender", "wdfilter"],
301
+ "responder": ["responder", "llmnr", "nbt-ns", "netbios"],
302
+ "suid": ["suid", "setuid", "set-uid"],
303
+ "cron": ["cron", "crontab", "cronjob", "scheduled task"],
304
+ "lolbin": ["lolbin", "lolbas", "living off the land"],
305
+ "c2": ["c2", "command and control", "command-and-control", "beacon"],
306
+ "sliver": ["sliver", "sliver c2"],
307
+ "cobalt": ["cobalt strike", "cobalt", "cs beacon"],
308
+ "phishing": ["phishing", "spearphishing", "social engineering"],
309
+ "forensics": ["forensics", "forensic", "dfir"],
310
+ "volatility": ["volatility", "memory forensics", "memory analysis"],
311
+ "steganography": ["steganography", "stego", "steghide"],
312
+ "stego": ["steganography", "stego", "steghide"],
313
+ "rbcd": ["resource-based constrained delegation", "rbcd"],
314
+ "dpapi": ["dpapi", "data protection api", "credential manager"],
315
+ "printnightmare": ["printnightmare", "cve-2021-34527", "spoolsv", "printspooler"],
316
+ "cve-2021-34527": ["printnightmare", "cve-2021-34527", "spoolsv"],
317
+ "eternalblue": ["eternalblue", "ms17-010", "smbv1"],
318
+ "ms17-010": ["eternalblue", "ms17-010", "smbv1"],
319
+ "pwnkit": ["pwnkit", "cve-2021-4034", "pkexec"],
320
+ "cve-2021-4034": ["pwnkit", "cve-2021-4034", "pkexec"],
321
+ "log4shell": ["log4shell", "cve-2021-44228", "log4j"],
322
+ "cve-2021-44228": ["log4shell", "cve-2021-44228", "log4j"],
323
+ "zerologon": ["zerologon", "cve-2020-1472", "netlogon"],
324
+ "cve-2020-1472": ["zerologon", "cve-2020-1472", "netlogon"],
325
+ "petitpotam": ["petitpotam", "cve-2021-36942", "efs", "ntlm relay"],
326
+ "certifried": ["certifried", "cve-2022-26923", "adcs"],
327
+ "nopac": ["nopac", "samaccountname", "cve-2021-42278", "cve-2021-42287"],
328
+ "proxylogon": ["proxylogon", "cve-2021-26855", "exchange"],
329
+ "proxyshell": ["proxyshell", "cve-2021-34473", "exchange"],
330
+ }
331
+
332
+
333
+ # ============================================================================
334
+ # CONFIG DATACLASS
335
+ # ============================================================================
336
+
337
+
338
+ def _resolve_path(raw, default: Path) -> Path:
339
+ """Resolve a path from YAML (string) or use default (Path)."""
340
+ if raw is None:
341
+ return default
342
+ p = Path(raw)
343
+ if not p.is_absolute():
344
+ p = BASE_DIR / p
345
+ return p
346
+
347
+
348
+ @dataclass
349
+ class Config:
350
+ """Central configuration for the RAG system — loads from config.yaml when available."""
351
+
352
+ # Paths
353
+ data_dir: Path = field(default_factory=lambda: _resolve_path(_get("paths", "data_dir", None), BASE_DIR / "data"))
354
+ chroma_dir: Path = field(
355
+ default_factory=lambda: _resolve_path(_get("paths", "data_dir", None), BASE_DIR / "data") / "chroma_db"
356
+ )
357
+ documents_dir: Path = field(
358
+ default_factory=lambda: _resolve_path(_get("paths", "documents_dir", None), BASE_DIR / "documents")
359
+ )
360
+
361
+ # Chunking
362
+ chunk_size: int = field(
363
+ default_factory=lambda: (
364
+ _get("documents", "chunking", {}).get("chunk_size", 1000)
365
+ if isinstance(_get("documents", "chunking", {}), dict)
366
+ else 1000
367
+ )
368
+ )
369
+ chunk_overlap: int = field(
370
+ default_factory=lambda: (
371
+ _get("documents", "chunking", {}).get("chunk_overlap", 200)
372
+ if isinstance(_get("documents", "chunking", {}), dict)
373
+ else 200
374
+ )
375
+ )
376
+
377
+ # Embeddings
378
+ embedding_model: str = field(
379
+ default_factory=lambda: (
380
+ _get("models", "embedding", {}).get("model", "BAAI/bge-small-en-v1.5")
381
+ if isinstance(_get("models", "embedding", {}), dict)
382
+ else "BAAI/bge-small-en-v1.5"
383
+ )
384
+ )
385
+ embedding_dim: int = field(
386
+ default_factory=lambda: (
387
+ _get("models", "embedding", {}).get("dimensions", 384)
388
+ if isinstance(_get("models", "embedding", {}), dict)
389
+ else 384
390
+ )
391
+ )
392
+
393
+ # Reranker
394
+ reranker_model: str = field(
395
+ default_factory=lambda: (
396
+ _get("models", "reranker", {}).get("model", "Xenova/ms-marco-MiniLM-L-6-v2")
397
+ if isinstance(_get("models", "reranker", {}), dict)
398
+ else "Xenova/ms-marco-MiniLM-L-6-v2"
399
+ )
400
+ )
401
+ reranker_enabled: bool = field(
402
+ default_factory=lambda: (
403
+ _get("models", "reranker", {}).get("enabled", True)
404
+ if isinstance(_get("models", "reranker", {}), dict)
405
+ else True
406
+ )
407
+ )
408
+ reranker_top_k_multiplier: int = field(
409
+ default_factory=lambda: (
410
+ _get("models", "reranker", {}).get("top_k_multiplier", 3)
411
+ if isinstance(_get("models", "reranker", {}), dict)
412
+ else 3
413
+ )
414
+ )
415
+
416
+ # ChromaDB
417
+ collection_name: str = field(default_factory=lambda: _get("search", "collection_name", "knowledge_base"))
418
+
419
+ # Supported formats
420
+ supported_formats: List[str] = field(
421
+ default_factory=lambda: _get(
422
+ "documents", "supported_formats", [".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv"]
423
+ )
424
+ )
425
+
426
+ # Category mappings
427
+ category_mappings: Dict[str, str] = field(
428
+ default_factory=lambda: _get_top("category_mappings", _DEFAULT_CATEGORY_MAPPINGS)
429
+ )
430
+
431
+ # Keyword routes
432
+ keyword_routes: Dict[str, List[str]] = field(
433
+ default_factory=lambda: _get_top("keyword_routes", _DEFAULT_KEYWORD_ROUTES)
434
+ )
435
+
436
+ # Query expansions
437
+ query_expansions: Dict[str, List[str]] = field(
438
+ default_factory=lambda: _get_top("query_expansions", _DEFAULT_QUERY_EXPANSIONS)
439
+ )
440
+
441
+ # Search settings
442
+ default_results: int = field(default_factory=lambda: _get("search", "default_results", 5))
443
+ max_results: int = field(default_factory=lambda: _get("search", "max_results", 20))
444
+
445
+ def __post_init__(self):
446
+ """Validate config values and ensure directories exist."""
447
+ # Bounds validation
448
+ if not isinstance(self.chunk_size, int) or self.chunk_size < 100:
449
+ print(f"[WARN] chunk_size={self.chunk_size} invalid, using 1000")
450
+ self.chunk_size = 1000
451
+ if not isinstance(self.chunk_overlap, int) or self.chunk_overlap < 0:
452
+ print(f"[WARN] chunk_overlap={self.chunk_overlap} invalid, using 200")
453
+ self.chunk_overlap = 200
454
+ if self.chunk_overlap >= self.chunk_size:
455
+ print(
456
+ f"[WARN] chunk_overlap ({self.chunk_overlap}) >= chunk_size ({self.chunk_size}), using {self.chunk_size // 5}"
457
+ )
458
+ self.chunk_overlap = self.chunk_size // 5
459
+ if not isinstance(self.default_results, int) or self.default_results < 1:
460
+ self.default_results = 5
461
+ if not isinstance(self.max_results, int) or self.max_results < 1:
462
+ self.max_results = 20
463
+ if not isinstance(self.embedding_dim, int) or self.embedding_dim < 1:
464
+ self.embedding_dim = 384
465
+ if not isinstance(self.reranker_enabled, bool):
466
+ print(f"[WARN] reranker_enabled={self.reranker_enabled!r} invalid, using True")
467
+ self.reranker_enabled = True
468
+ if not isinstance(self.reranker_top_k_multiplier, int) or self.reranker_top_k_multiplier < 1:
469
+ self.reranker_top_k_multiplier = 3
470
+ if not isinstance(self.supported_formats, list) or not self.supported_formats:
471
+ print("[WARN] supported_formats is empty or invalid, using defaults")
472
+ self.supported_formats = [".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv"]
473
+
474
+ # Validate keyword_routes values are lists (not strings)
475
+ for cat, keywords in list(self.keyword_routes.items()):
476
+ if not isinstance(keywords, list):
477
+ print(f"[WARN] keyword_routes.{cat} is not a list, removing")
478
+ del self.keyword_routes[cat]
479
+
480
+ # Ensure directories exist
481
+ self.data_dir.mkdir(parents=True, exist_ok=True)
482
+ self.chroma_dir.mkdir(parents=True, exist_ok=True)
483
+ self.documents_dir.mkdir(parents=True, exist_ok=True)
484
+
485
+
486
+ # Global config instance
487
+ config = Config()
@@ -19,10 +19,8 @@ Features:
19
19
  - CRUD operations via MCP tools (add, update, remove docs)
20
20
 
21
21
  Autor: Lyon (Ailton Rocha)
22
- Versao: 3.0.0
23
- Data: 2026-03-19
24
-
25
- By Lyon :) Legal Ne?
22
+ Versao: 3.3.2
23
+ Data: 2026-04-06
26
24
  """
27
25
 
28
26
  import hashlib
@@ -1827,8 +1825,58 @@ def evaluate_retrieval(test_cases: str) -> str:
1827
1825
  # =============================================================================
1828
1826
 
1829
1827
 
1828
+ def _handle_init():
1829
+ """Export config template and presets to current directory."""
1830
+ import shutil
1831
+
1832
+ data_dir = Path(__file__).parent / "data"
1833
+ if not data_dir.exists():
1834
+ print("[ERROR] Bundled data not found. If installed from git, use presets/ directly.")
1835
+ return
1836
+
1837
+ cwd = Path.cwd()
1838
+
1839
+ try:
1840
+ # Copy config.example.yaml
1841
+ src = data_dir / "config.example.yaml"
1842
+ if src.exists():
1843
+ dst = cwd / "config.example.yaml"
1844
+ shutil.copy2(src, dst)
1845
+ print(f"[OK] {dst}")
1846
+
1847
+ # Copy presets
1848
+ presets_dir = cwd / "presets"
1849
+ presets_dir.mkdir(exist_ok=True)
1850
+ for f in data_dir.glob("*.yaml"):
1851
+ if f.name == "config.example.yaml":
1852
+ continue
1853
+ dst = presets_dir / f.name
1854
+ shutil.copy2(f, dst)
1855
+ print(f"[OK] {dst}")
1856
+
1857
+ # Create documents dir
1858
+ docs_dir = cwd / "documents"
1859
+ docs_dir.mkdir(exist_ok=True)
1860
+ print(f"[OK] {docs_dir}/")
1861
+
1862
+ print("\nDone. Quick start:")
1863
+ print(" cp presets/general.yaml config.yaml # or cybersecurity, developer, research")
1864
+ print(" # Add your documents to documents/")
1865
+ print(" # Restart Claude Code")
1866
+ except PermissionError:
1867
+ print("[ERROR] Permission denied. Run from a writable directory.")
1868
+ except OSError as e:
1869
+ print(f"[ERROR] Failed to write files: {e}")
1870
+
1871
+
1830
1872
  def main():
1831
1873
  """Run the MCP server"""
1874
+ import sys
1875
+
1876
+ if len(sys.argv) > 1 and sys.argv[1] == "init":
1877
+ _handle_init()
1878
+ return
1879
+
1832
1880
  orchestrator = get_orchestrator()
1833
1881
 
1834
1882
  # Migration: check dimension mismatch AFTER full init (avoids segfault during __init__)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "knowledge-rag"
7
- version = "3.3.0"
7
+ version = "3.3.2"
8
8
  description = "Local RAG System for Claude Code — Hybrid search + Cross-encoder Reranking + 12 MCP Tools. Zero external servers."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -55,6 +55,13 @@ knowledge-rag = "mcp_server.server:main"
55
55
  [tool.hatch.build.targets.wheel]
56
56
  packages = ["mcp_server"]
57
57
 
58
+ [tool.hatch.build.targets.wheel.force-include]
59
+ "config.example.yaml" = "mcp_server/data/config.example.yaml"
60
+ "presets/cybersecurity.yaml" = "mcp_server/data/cybersecurity.yaml"
61
+ "presets/developer.yaml" = "mcp_server/data/developer.yaml"
62
+ "presets/research.yaml" = "mcp_server/data/research.yaml"
63
+ "presets/general.yaml" = "mcp_server/data/general.yaml"
64
+
58
65
  [tool.hatch.build.targets.sdist]
59
66
  include = [
60
67
  "mcp_server/",
@@ -1,278 +0,0 @@
1
- """Configuration for Knowledge RAG System v3.1 — YAML-configurable"""
2
-
3
- import os
4
- from dataclasses import dataclass, field
5
- from pathlib import Path
6
- from typing import Dict, List
7
-
8
- import yaml
9
-
10
- # ============================================================================
11
- # BASE DIRECTORY RESOLUTION
12
- # ============================================================================
13
- # Priority: 1. KNOWLEDGE_RAG_DIR env var 2. Source checkout 3. CWD
14
-
15
- _source_dir = Path(__file__).parent.parent
16
-
17
-
18
- _SUPPORTED_SUFFIXES = frozenset([".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv"])
19
-
20
-
21
- def _has_documents(path: Path) -> bool:
22
- """Check if path has a documents/ dir with actual supported files (follows symlinks)."""
23
- docs_dir = path / "documents"
24
- if not docs_dir.exists():
25
- return False
26
- for root, _, files in os.walk(docs_dir, followlinks=True):
27
- for f in files:
28
- if Path(f).suffix.lower() in _SUPPORTED_SUFFIXES:
29
- return True
30
- return False
31
-
32
-
33
- if os.environ.get("KNOWLEDGE_RAG_DIR"):
34
- BASE_DIR = Path(os.environ["KNOWLEDGE_RAG_DIR"])
35
- elif _has_documents(_source_dir):
36
- BASE_DIR = _source_dir
37
- elif _has_documents(Path.cwd()):
38
- BASE_DIR = Path.cwd()
39
- else:
40
- BASE_DIR = Path.cwd()
41
-
42
-
43
- # ============================================================================
44
- # YAML CONFIG LOADER
45
- # ============================================================================
46
-
47
- def _load_yaml_config() -> dict:
48
- """Load config.yaml from BASE_DIR if it exists, otherwise return empty dict."""
49
- config_path = BASE_DIR / "config.yaml"
50
- if not config_path.exists():
51
- return {}
52
-
53
- try:
54
- with open(config_path, "r", encoding="utf-8") as f:
55
- data = yaml.safe_load(f)
56
- if not isinstance(data, dict):
57
- print(f"[WARN] config.yaml is not a valid mapping, ignoring")
58
- return {}
59
- print(f"[INFO] Loaded config from {config_path}")
60
- return data
61
- except yaml.YAMLError as e:
62
- print(f"[WARN] Failed to parse config.yaml: {e} — using defaults")
63
- return {}
64
-
65
-
66
- _yaml = _load_yaml_config()
67
-
68
-
69
- def _get(section: str, key: str, default):
70
- """Get a value from the YAML config with section.key path, falling back to default."""
71
- s = _yaml.get(section, {})
72
- if not isinstance(s, dict):
73
- return default
74
- val = s.get(key)
75
- return val if val is not None else default
76
-
77
-
78
- # ============================================================================
79
- # DEFAULTS (used when no config.yaml or field is omitted)
80
- # ============================================================================
81
-
82
- _DEFAULT_CATEGORY_MAPPINGS = {
83
- "security/redteam": "redteam",
84
- "security/blueteam": "blueteam",
85
- "security/ctf": "ctf",
86
- "security": "security",
87
- "aar": "aar",
88
- "logscale": "logscale",
89
- "development": "development",
90
- "general": "general",
91
- }
92
-
93
- _DEFAULT_KEYWORD_ROUTES = {
94
- "logscale": [
95
- "logscale", "lql", "cql", "humio", "crowdstrike query",
96
- "formattime", "groupby", "base64decode", "case{}", "regex",
97
- ],
98
- "redteam": [
99
- "pentest", "exploit", "payload", "reverse shell", "privilege escalation",
100
- "lateral movement", "c2", "beacon", "cobalt strike", "metasploit",
101
- "gtfobins", "lolbas", "lolbin", "suid", "sudo", "byovd", "lol driver",
102
- "lolad", "lolapps", "hacktricks", "privesc", "kerberoast", "dcsync",
103
- "golden ticket", "pass-the-hash", "bloodhound", "mimikatz", "rubeus",
104
- "certipy", "adcs", "sqli", "xss", "ssti", "ssrf", "lfi", "rfi", "xxe",
105
- "deserialization", "ysoserial", "upload bypass", "reverse shell",
106
- "web shell", "hash cracking", "hashcat", "waf bypass", "amsi bypass",
107
- "uac bypass", "potato", "searchsploit", "exploit-db", "cve",
108
- ],
109
- "blueteam": [
110
- "detection", "sigma", "yara", "ioc", "threat hunting",
111
- "incident response", "forensics", "malware analysis",
112
- ],
113
- "ctf": [
114
- "ctf", "flag", "hackthebox", "htb", "tryhackme",
115
- "picoctf", "writeup", "challenge",
116
- ],
117
- "development": [
118
- "python", "typescript", "javascript", "api",
119
- "fastapi", "django", "react", "nodejs",
120
- ],
121
- "security": [
122
- "anti-bot", "antibot", "js challenge", "javascript challenge",
123
- "cdp detection", "runtime.enable", "puppeteer", "playwright",
124
- "selenium", "nodriver", "stealth", "undetected", "ja3", "ja4",
125
- "tls fingerprint", "fingerprinting", "curl_cffi", "got-scraping",
126
- "impersonate", "http/2 settings", "browser fingerprint",
127
- "canvas fingerprint", "webgl fingerprint", "navigator.webdriver",
128
- "audio context", "hardware concurrency", "waf bypass", "aws waf",
129
- "cloudflare bypass", "akamai bypass", "datadome", "perimeterx",
130
- "imperva bypass", "8kb bypass", "body size limit", "json sqli",
131
- "behavioral", "mouse movement", "ghost-cursor", "humanized",
132
- "flaresolverr", "turnstile", "rebrowser", "botbrowser",
133
- ],
134
- }
135
-
136
- _DEFAULT_QUERY_EXPANSIONS = {
137
- "sqli": ["sql injection", "sqli"],
138
- "sql injection": ["sql injection", "sqli"],
139
- "xss": ["cross-site scripting", "xss"],
140
- "cross-site scripting": ["cross-site scripting", "xss"],
141
- "ssrf": ["server-side request forgery", "ssrf"],
142
- "lfi": ["local file inclusion", "lfi"],
143
- "rfi": ["remote file inclusion", "rfi"],
144
- "rce": ["remote code execution", "rce"],
145
- "xxe": ["xml external entity", "xxe"],
146
- "ssti": ["server-side template injection", "ssti"],
147
- "idor": ["insecure direct object reference", "idor"],
148
- "csrf": ["cross-site request forgery", "csrf"],
149
- "privesc": ["privilege escalation", "privesc"],
150
- "priv esc": ["privilege escalation", "privesc"],
151
- "privilege escalation": ["privilege escalation", "privesc"],
152
- "deserialization": ["deserialization", "deserialisation", "insecure deserialization"],
153
- "pth": ["pass-the-hash", "pth"],
154
- "pass-the-hash": ["pass-the-hash", "pth"],
155
- "dcsync": ["dcsync", "dc sync", "domain controller sync"],
156
- "kerberoast": ["kerberoasting", "kerberoast"],
157
- "kerberoasting": ["kerberoasting", "kerberoast"],
158
- "asrep": ["as-rep roasting", "asrep", "asreproast"],
159
- "bloodhound": ["bloodhound", "sharphound"],
160
- "mimikatz": ["mimikatz", "sekurlsa", "logonpasswords"],
161
- "hashcat": ["hashcat", "hash cracking", "hash crack"],
162
- "john": ["john the ripper", "john", "jtr"],
163
- "revshell": ["reverse shell", "revshell", "rev shell"],
164
- "reverse shell": ["reverse shell", "revshell"],
165
- "webshell": ["web shell", "webshell"],
166
- "web shell": ["web shell", "webshell"],
167
- "waf": ["web application firewall", "waf"],
168
- "amsi": ["antimalware scan interface", "amsi", "amsi bypass"],
169
- "uac": ["user account control", "uac", "uac bypass"],
170
- "potato": ["potato", "juicypotato", "sweetpotato", "godpotato", "efspotato", "printspoofer"],
171
- "ntlm": ["ntlm", "net-ntlmv2", "ntlmv2"],
172
- "smb": ["smb", "server message block", "samba"],
173
- "ldap": ["ldap", "lightweight directory access protocol"],
174
- "ad": ["active directory", "ad"],
175
- "active directory": ["active directory", "ad"],
176
- "defender": ["windows defender", "defender", "wdfilter"],
177
- "responder": ["responder", "llmnr", "nbt-ns", "netbios"],
178
- "suid": ["suid", "setuid", "set-uid"],
179
- "cron": ["cron", "crontab", "cronjob", "scheduled task"],
180
- "lolbin": ["lolbin", "lolbas", "living off the land"],
181
- "c2": ["c2", "command and control", "command-and-control", "beacon"],
182
- "sliver": ["sliver", "sliver c2"],
183
- "cobalt": ["cobalt strike", "cobalt", "cs beacon"],
184
- "phishing": ["phishing", "spearphishing", "social engineering"],
185
- "forensics": ["forensics", "forensic", "dfir"],
186
- "volatility": ["volatility", "memory forensics", "memory analysis"],
187
- "steganography": ["steganography", "stego", "steghide"],
188
- "stego": ["steganography", "stego", "steghide"],
189
- "rbcd": ["resource-based constrained delegation", "rbcd"],
190
- "dpapi": ["dpapi", "data protection api", "credential manager"],
191
- "printnightmare": ["printnightmare", "cve-2021-34527", "spoolsv", "printspooler"],
192
- "cve-2021-34527": ["printnightmare", "cve-2021-34527", "spoolsv"],
193
- "eternalblue": ["eternalblue", "ms17-010", "smbv1"],
194
- "ms17-010": ["eternalblue", "ms17-010", "smbv1"],
195
- "pwnkit": ["pwnkit", "cve-2021-4034", "pkexec"],
196
- "cve-2021-4034": ["pwnkit", "cve-2021-4034", "pkexec"],
197
- "log4shell": ["log4shell", "cve-2021-44228", "log4j"],
198
- "cve-2021-44228": ["log4shell", "cve-2021-44228", "log4j"],
199
- "zerologon": ["zerologon", "cve-2020-1472", "netlogon"],
200
- "cve-2020-1472": ["zerologon", "cve-2020-1472", "netlogon"],
201
- "petitpotam": ["petitpotam", "cve-2021-36942", "efs", "ntlm relay"],
202
- "certifried": ["certifried", "cve-2022-26923", "adcs"],
203
- "nopac": ["nopac", "samaccountname", "cve-2021-42278", "cve-2021-42287"],
204
- "proxylogon": ["proxylogon", "cve-2021-26855", "exchange"],
205
- "proxyshell": ["proxyshell", "cve-2021-34473", "exchange"],
206
- }
207
-
208
-
209
- # ============================================================================
210
- # CONFIG DATACLASS
211
- # ============================================================================
212
-
213
- def _resolve_path(raw, default: Path) -> Path:
214
- """Resolve a path from YAML (string) or use default (Path)."""
215
- if raw is None:
216
- return default
217
- p = Path(raw)
218
- if not p.is_absolute():
219
- p = BASE_DIR / p
220
- return p
221
-
222
-
223
- @dataclass
224
- class Config:
225
- """Central configuration for the RAG system — loads from config.yaml when available."""
226
-
227
- # Paths
228
- data_dir: Path = field(default_factory=lambda: _resolve_path(
229
- _get("paths", "data_dir", None), BASE_DIR / "data"
230
- ))
231
- chroma_dir: Path = field(default_factory=lambda: _resolve_path(
232
- _get("paths", "data_dir", None), BASE_DIR / "data"
233
- ) / "chroma_db")
234
- documents_dir: Path = field(default_factory=lambda: _resolve_path(
235
- _get("paths", "documents_dir", None), BASE_DIR / "documents"
236
- ))
237
-
238
- # Chunking
239
- chunk_size: int = field(default_factory=lambda: _get("documents", "chunking", {}).get("chunk_size", 1000) if isinstance(_get("documents", "chunking", {}), dict) else 1000)
240
- chunk_overlap: int = field(default_factory=lambda: _get("documents", "chunking", {}).get("chunk_overlap", 200) if isinstance(_get("documents", "chunking", {}), dict) else 200)
241
-
242
- # Embeddings
243
- embedding_model: str = field(default_factory=lambda: _get("models", "embedding", {}).get("model", "BAAI/bge-small-en-v1.5") if isinstance(_get("models", "embedding", {}), dict) else "BAAI/bge-small-en-v1.5")
244
- embedding_dim: int = field(default_factory=lambda: _get("models", "embedding", {}).get("dimensions", 384) if isinstance(_get("models", "embedding", {}), dict) else 384)
245
-
246
- # Reranker
247
- reranker_model: str = field(default_factory=lambda: _get("models", "reranker", {}).get("model", "Xenova/ms-marco-MiniLM-L-6-v2") if isinstance(_get("models", "reranker", {}), dict) else "Xenova/ms-marco-MiniLM-L-6-v2")
248
- reranker_enabled: bool = field(default_factory=lambda: _get("models", "reranker", {}).get("enabled", True) if isinstance(_get("models", "reranker", {}), dict) else True)
249
- reranker_top_k_multiplier: int = field(default_factory=lambda: _get("models", "reranker", {}).get("top_k_multiplier", 3) if isinstance(_get("models", "reranker", {}), dict) else 3)
250
-
251
- # ChromaDB
252
- collection_name: str = field(default_factory=lambda: _get("search", "collection_name", "knowledge_base"))
253
-
254
- # Supported formats
255
- supported_formats: List[str] = field(default_factory=lambda: _get("documents", "supported_formats", [".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv"]))
256
-
257
- # Category mappings
258
- category_mappings: Dict[str, str] = field(default_factory=lambda: _yaml.get("category_mappings", _DEFAULT_CATEGORY_MAPPINGS))
259
-
260
- # Keyword routes
261
- keyword_routes: Dict[str, List[str]] = field(default_factory=lambda: _yaml.get("keyword_routes", _DEFAULT_KEYWORD_ROUTES))
262
-
263
- # Query expansions
264
- query_expansions: Dict[str, List[str]] = field(default_factory=lambda: _yaml.get("query_expansions", _DEFAULT_QUERY_EXPANSIONS))
265
-
266
- # Search settings
267
- default_results: int = field(default_factory=lambda: _get("search", "default_results", 5))
268
- max_results: int = field(default_factory=lambda: _get("search", "max_results", 20))
269
-
270
- def __post_init__(self):
271
- """Ensure directories exist"""
272
- self.data_dir.mkdir(parents=True, exist_ok=True)
273
- self.chroma_dir.mkdir(parents=True, exist_ok=True)
274
- self.documents_dir.mkdir(parents=True, exist_ok=True)
275
-
276
-
277
- # Global config instance
278
- config = Config()
File without changes
File without changes